-
Notifications
You must be signed in to change notification settings - Fork 0
Recipes
Recipes are a way to plug in pre-defined test steps into your test pipeline.
This allows for:
- step reuse
- step composition
GivenRecipe (this is immediately after Scenario())
AndRecipe
ButRecipe
WhenRecipe
ThenRecipe
Each recipe function has three overloads:
- Recipes that implement
RecipeStep<T, R>
- Recipes that implement
RecipeStep<T>
(the same asRecipeStep<T, T>
) - Recipes that implement
Func<Pipe<T>, Pipe<R>>
The RecipeStep<T, R>
and RecipeStep<T>
overloads are for single steps that are pluggable into any of the recipe functions and are made to not be aware of the step type they will be used for. The step type is decided by the recipe function using it. This is also the best way to compose other recipe steps (of any step type) within this function as the entire set of steps can be also plugged into any step type.
The Func<Pipe<T>, Pipe<R>>
overload returns a Pipe<>
. This gives the function the ability to call anything that is normally available starting with the last Pipe<>
instance. This is not tied to the step type of the calling function so it's up to the internal implementation to suit.
If running steps to setup the test, you could have many of the following kind of steps that have a test setup context (in this example named ScenarioInfo):
public static RecipeStep<ScenarioInfo> ScenarioStepOne() =>
recipe =>
recipe.Step("scenario step one", scenarioInfo =>
{
// do some setup
//scenarioInfo.Setup = ...
});
Here are two recipes which are defined in the static class TestRecipes
Scenario()
.GivenRecipe(TestRecipes.WithNewUserContext())
.AndRecipe(TestRecipes.WithSignUp())
.When("... continue as normal ...", scenarioInfo => {})
If using static TestRecipes;
is used, the Recipes prefix can be dropped:
.GivenRecipe(WithNewUserContext())
.AndRecipe(WithSignUp())
public static RecipeStep<Scenario, ScenarioInfo> WithNewUserContext() =>
recipe =>
recipe.Step("a new user context",
scenario => new ScenarioInfo(/* with a new user context */)
);
public static RecipeStep<ScenarioInfo> WithSignUp() =>
recipe =>
recipe.Step("the user signs up with a chosen email and password", async scenarioInfo =>
{
// sign up the user with scenarioInfo.UserContext
});
Immediately after a call to Scenario() using GivenRecipe() it would not be ideal to use the given step solely to create ScenarioInfo. If that was the case then all the given steps would read something like "Given a new instance of scenario info is created". It is better to have something meaningful happen in the Given step and create the instance behind the scenes.
A function such as the custom GivenRecipe
or AndRecipe
overloads below allow for any recipe method to plug in and be given the initial ScenarioInfo
instance. They do not come with the library so would need to be copied into a project that requires it.
// Allow plugging of RecipeStep<ScenarioInfo> seamlessly into recipe methods in tests
public static partial class Recipes
{
private static RecipeStep<BddPipe.Scenario, ScenarioInfo> FromNewScenarioInfo(this RecipeStep<ScenarioInfo> initialStep) =>
recipe =>
initialStep(
// call the initial step with a mapped input argument, not Scenario, but ScenarioInfo.
recipe.Map(scenario => new ScenarioInfo())
);
public static Pipe<ScenarioInfo> GivenRecipe(this Pipe<BddPipe.Scenario> scenario, RecipeStep<ScenarioInfo> initialStep)
{
return scenario.GivenRecipe(initialStep.FromNewScenarioInfo());
}
public static Pipe<ScenarioInfo> AndRecipe(this Pipe<BddPipe.Scenario> scenario, RecipeStep<ScenarioInfo> initialStep)
{
return scenario.AndRecipe(initialStep.FromNewScenarioInfo());
}
}
Just as Recipes can be used in a test method, they can also be used to make a single Recipe function that is composed of many Recipe steps.
Recipe composition functions can also call others. Any of these methods can plug into any of the recipe step types because of how the initial RecipeStep<ScenarioInfo>
style step is simply invoked with the initial argument (which is already tied to the calling recipe function).
public static partial class TestRecipes
{
public static RecipeStep<ScenarioInfo> WithFirstSecondAndThird() =>
recipe => WithFirst()(recipe)
.AndRecipe(WithSecond())
.AndRecipe(WithThird());
public static RecipeStep<ScenarioInfo> WithFirstFour() =>
recipe => WithFirstSecondAndThird()(recipe)
.AndRecipe(WithFourth());
}
Recipes can have parameters - they are just methods that produce a recipe function. The test can provide the Recipe with custom step titles, extra data or alterations to internal setup (via delegates) to make the step have a dynamic behaviour.
.AndRecipe(WithMemberPinHistoryAdd(
title: "a five year member pin is created",
modifier: (scenarioInfo, record) => new MemberPinHistoryAdd(
memberId: record.MemberId,
addedBy: record.MemberIdAddedBy,
atDate: record.AtDate,
years: 5,
createdDateUtc: record.CreatedDateUtc.AddDays(1)
)
))
[Test]
public void Login_CorrectUserNameInvalidPassword_Error()
{
Scenario()
.GivenRecipe(GivenNewUserContext())
.AndRecipe(AndSignUp())
.AndRecipe(AndSignUpVerified())
.When("Login is called with the signed up email and an INVALID password", async scenarioInfo =>
{
var request = new LoginRequest(scenarioInfo.EmailAddress, "invalidpassword");
var response = await ApiCalls.Auth.Login(request);
return new { Request = request, Response = response };
})
.Then("the login response is not successful", outcome =>
{
outcome.Response.ShouldBeSuccessful(loginResult =>
{
loginResult.ShouldBeFailure();
});
})
.Run();
}
If you wish to ensure steps are not called out of order, one approach is via Require.
public static Func<Pipe<ScenarioInfo>, Pipe<ScenarioInfo>> AndSignUpVerified() =>
pipe =>
pipe.And("user sign up verification code is verified", async scenarioInfo =>
{
Require(() => scenarioInfo.CurrentUserContext.SignUps.Count > 0);
// ... there must be at least one signup in the scenario info to proceed ..
public static void Require(Func<bool> predicate, [CallerMemberName] string caller = null)
{
if (!predicate()) { Assert.Inconclusive($"Pre-requisite expected for {caller ?? "test setup"}"); }
}
- Recipes that require a
RecipeStep<T>
orRecipeStep<T, R>
delegate.
Pipe<R> GivenRecipe<R>(RecipeStep<Unit, R> recipeStepFunc)
Pipe<R> GivenRecipe<R>(this Scenario scenario, RecipeStep<Scenario, R> recipeStepFunc)
Pipe<R> AndRecipe<T, R>(this Pipe<T> pipe, RecipeStep<T, R> recipeStepFunc)
Pipe<T> AndRecipe<T>(this Pipe<T> pipe, RecipeStep<T> recipeStepFunc)
Pipe<R> WhenRecipe<T, R>(this Pipe<T> pipe, RecipeStep<T, R> recipeStepFunc)
Pipe<T> WhenRecipe<T>(this Pipe<T> pipe, RecipeStep<T> recipeStepFunc)
Pipe<R> ButRecipe<T, R>(this Pipe<T> pipe, RecipeStep<T, R> recipeStepFunc)
Pipe<T> ButRecipe<T>(this Pipe<T> pipe, RecipeStep<T> recipeStepFunc)
Pipe<R> ThenRecipe<T, R>(this Pipe<T> pipe, RecipeStep<T, R> recipeStepFunc)
Pipe<T> ThenRecipe<T>(this Pipe<T> pipe, RecipeStep<T> recipeStepFunc)
- Recipes that require a function where the
Pipe<>
is both passed and expected as a result.
Pipe<R> GivenRecipe<R>(this Scenario scenario, Func<Scenario, Pipe<R>> recipeFunction)
Pipe<R> AndRecipe<T, R>(this Pipe<T> pipe, Func<Pipe<T>, Pipe<R>> recipeFunction)
Pipe<R> WhenRecipe<T, R>(this Pipe<T> pipe, Func<Pipe<T>, Pipe<R>> recipeFunction)
Pipe<R> ButRecipe<T, R>(this Pipe<T> pipe, Func<Pipe<T>, Pipe<R>> recipeFunction)
Pipe<R> ThenRecipe<T, R>(this Pipe<T> pipe, Func<Pipe<T>, Pipe<R>> recipeFunction)