Skip to content

Scenario Steps Definition (v2.x)

Suremaker edited this page Jan 13, 2019 · 1 revision

Page version: 3.x / 2.x / 1.x

Supported step definitions

LightBDD offers various ways to define scenario steps as well as support various step definitions in order to best balance scenario clarity with development needs.

All currently supported step definitions can be found by using one or more namespaces from the LightBDD.Framework.Scenarios namespace. When chosen, extension methods defined in the namespace will be will become available for the Runner property.

Basic scenarios

Namespace: LightBDD.Framework.Scenarios.Basic

Basic scenarios offers the best clarity at the cost of functionality:

Runner.RunScenario(

	Given_the_user_is_about_to_login,
	Given_the_user_entered_valid_login,
	Given_the_user_entered_valid_password,
	When_the_user_clicks_login_button,
	Then_the_login_operation_should_be_successful,
	Then_a_welcome_message_containing_user_name_should_be_returned);

/* ... */
private void Given_the_user_is_about_to_login() { /* ... */ }

Scenario steps are defined as an array of delegates to step methods, where each method:

  • is a test class instance or a static method,
  • is returning void,
  • is parameterless,
  • cannot be a lambda expression nor anonymous method (while this constraint would not be enforced, when violated, the step name would be incorrectly determined),
  • does not support contextual scenarios.

The example of basic scenarios can be found here.

Extended scenarios

Namespace: LightBDD.Framework.Scenarios.Extended

Extended scenarios offers more flexibility than basic equivalents:

  • they support parameterized steps,
  • they support contextual scenarios.
Runner.RunScenario(

	given => Product_is_available_in_product_storage("wooden desk"),
	and   => Product_is_available_in_product_storage("wooden shelf"),
	when  => Customer_buys_product("wooden desk"),
	and   => Customer_buys_product("wooden shelf"),
	then  => An_invoice_should_be_sent_to_the_customer(),
	and   => The_invoice_should_contain_product_with_price_of_AMOUNT("wooden desk", 62),
	and   => The_invoice_should_contain_product_with_price_of_AMOUNT("wooden shelf", 37));

/* ... */
private void Product_is_available_in_product_storage(string product)
{ /* ... */ }
Runner.RunScenario(

	_ => Given_product_is_available_in_product_storage("wooden desk"),
	_ => Given_product_is_available_in_product_storage("wooden shelf"),
	_ => When_customer_buys_product("wooden desk"),
	_ => When_customer_buys_product("wooden shelf"),
	_ => Then_an_invoice_should_be_sent_to_the_customer(),
	_ => Then_the_invoice_should_contain_product_with_price_of_AMOUNT("wooden desk", 62),
	_ => Then_the_invoice_should_contain_product_with_price_of_AMOUNT("wooden shelf", 37));

/* ... */
private void Given_product_is_available_in_product_storage(string product)
{ /* ... */ }

The two above examples are equivalent.

Scenario steps are defined as an array of single parameter lambda expressions, where for each expression:

  • lambda parameter defines the step type (given, when, then, etc.), unless the special _ name is used or since version 2.2.0 any single character name is used, then step type is determined from the method name,
  • lambda expression is a method call expression, where called method name corresponds to the step name,
  • step method can accept any amount of parameters of any type, as long as they are not ref nor out,
  • step type, step method name and passed parameters forms a step name.

The step type can be determined here in two ways:

  • from a lambda parameter given => ... which gives more control of defining step type, but leaves the methods with less clear names,
  • from the method name _ => Given_... or x => Given_... which gives a bit less control of the step type, but makes methods very clear.

Because of higher method clarity and Steps auto-grouping feature, the latter approach is suggested as the best.

The rules for formatting parameterized step name are described in Formatting Parameterized Steps section.

The example of extended, parameterized scenario can be found here.

Contextual scenarios

Namespace: LightBDD.Framework.Scenarios.Contextual

Normally, the scenarios are executing with no additional context and the step methods are defined in the test class. Sometimes, it is required however to execute the scenarios with an additional context:

  • if the test project is structured in a way that the step methods are defined in another class while the test class contains only scenario methods,
  • if the test framework offers the ability to run tests in parallel but the test methods share the same test class instance (like it is with NUnit3 or was with deprecated MbUnit).

Assuming that we have MyContext class, to make a scenario running with an instance of this context, one of following method has to be used:

Runner.WithContext<MyContext>().RunScenario(...);
Runner.WithContext(myContext).RunScenario(...);
Runner.WithContext(() = > new MyContext("abc")).RunScenario(...);

Please note that instances of the context will be created just before scenario execution, when LightBDD controls the creation of the context.

While the extension methods available in this namespace do not allow to run scenarios, they allow to modify scenarios to be contextual, which can be later used by other scenario types.

Disposable context

Since version 2.3.3, LightBDD can dispose the context implementing IDisposable interface when it is no longer needed.

The WithContext methods have been extended with new behavior where:

  • the .WithContext<MyContext>() will always dispose disposable context instances,
  • the .WithContext(new MyContext()) will not dispose context by default as instance is passed explicitly by caller. However, as the method has now an optional parameter bool takeOwnership = false, it is possible to make context automatically disposed by calling .WithContext(new MyContext(), true),
  • the .WithContext(() => new MyContext()) will dispose context by default as factory method is being passed indicating that it will produce a new instance on each call. Similarly to method above, it is possible to change default behavior and make context not disposed by calling .WithContext(() => new MyContext(), false).

Dependency injection

Since version 2.4.0, LightBDD supports DI containers and dependency injection for the context instances, where the WithContext<T>() methods has been modified to use DI container to instantiate the T instance.

To learn more about dependency injection in LightBDD, please read DI Containers section.

Extended scenarios with scenario context

The extended scenario types works well from contextual scenarios allowing to execute scenarios with additional context.

Runner.WithContext<ContactsManagementContext>().RunScenario(

    c => c.Given_my_contact_book_is_filled_with_contacts(),
    c => c.When_I_remove_one_contact(),
    c => c.Then_the_contact_book_should_not_contain_removed_contact_any_more(),
    c => c.Then_the_contact_book_should_contains_all_other_contacts());

/* ... */
public class ContactsManagementContext
{
    public void Given_my_contact_book_is_filled_with_contacts()
    {
        /* ... */
    }
    /* ... */
}

Please note that lambda parameter has a single letter c name in above example. Any single letter names, including _ will not participate in step name displayed in progress and final reports.

Runner.WithContext<SpeditionContext>().RunScenario(

    given => given.There_is_an_active_customer_with_id("ABC-123"),
    and => and.The_customer_has_product_in_basket("wooden shelf"),
    and => and.The_customer_has_product_in_basket("wooden desk"),
    when => when.The_customer_payment_finalizes(),
    then => then.Product_should_be_dispatched_to_the_customer("wooden shelf"),
    and => and.Product_should_be_dispatched_to_the_customer("wooden desk"));

/* ... */
public class SpeditionContext
{
    public void There_is_an_active_customer_with_id()
    {
        /* ... */
    }
    /* ... */
}

Scenario steps are defined in the same way as for regular Extended scenarios and have the same constraints, except that:

  • lambda expression parameter represents also the instance of the scenario context,
  • called step methods should be declared in context type (while it is more OO way and it is recommended, it is not enforced though),
  • context is instantiated before scenario execution and passed to each lambda delegate (it is shared with all steps of this scenario).

The example of contextual scenarios can be found here and here.

Fluent scenarios

Namespace: LightBDD.Framework.Scenarios.Fluent

Since 2.3.0 version, it is possible to define scenarios in a fluent way and mix basic, extended, async and sync steps together, however at the cost of test readability.

The use case for fluent scenario definitions is when the scenario is constructed in a programmatic manner (like when number of steps varies on input data or other dynamic reasons).

Example code:

using LightBDD.Framework;
using LightBDD.Framework.Scenarios.Basic;
using LightBDD.Framework.Scenarios.Extended;
using LightBDD.Framework.Scenarios.Fluent;
using LightBDD.NUnit3;

/* .. */

[Label("Ticket-14")]
[Scenario]
public async Task Browsing_invoices()
{
    await Runner
        .NewScenario()
        .AddAsyncSteps(
            _ => Given_invoice("Invoice-1"),
            _ => Given_invoice("Invoice-2"))
        .AddSteps(
            When_I_request_all_historical_invoices)
        .AddSteps(
            _ => Then_I_should_see_invoices("Invoice-1", "Invoice-2"))
        .RunAsync();
}

The fluent scenario definition is:

  • initiated with .NewScenario() method that can be called directly on Runner or Runner.WithContext(/* ... */),
  • finished with .RunAsync() that executes the scenario.

Note: The basic syntax is available only if .NewScenario() is called without context.

To see example usage, please check Invoice_history_feature.cs.

Compact scenarios

Namespace: LightBDD.Framework.Scenarios.Compact, LightBDD.Framework.Scenarios.Fluent

Since 2.5.0 version, it is possible to use compact steps to create self-contained scenarios.

The compact scenarios can be useful to produce more low-level, unit-test like scenarios, where decreased readability caused by keeping step implementation details within scenario method is an acceptable trade-off to the compact nature of the scenario.

Example code:

using LightBDD.Framework;
using LightBDD.Framework.Scenarios.Compact;
using LightBDD.Framework.Scenarios.Fluent;

/*...*/

[Scenario]
public async Task Adding_numbers()
{
    Calculator calc = null;
    var result = 0;

    await Runner
        .NewScenario()
        .AddStep("Given calculator", _ => calc = new Calculator())
        .AddStep("When I add two numbers", _ => result = calc.Add(3, 5))
        .AddStep("Then I should get an expected result", _ => Assert.Equal(8, result))
        .RunAsync();
}

Compact step definition:

  • it can be used with composite steps and fluent scenarios,
  • AddStep() / AddAsyncStep() adds one step at a time, where step consists of textual name and action to execute,
  • can be used with contextual scenarios/composites, where context is accessible by step action argument.

Please note that steps auto-grouping behavior is applicable to compact steps, which means that provided step names will be parsed the same way as for other syntax.

Steps auto-grouping

The step type (given, when, then, setup) is automatically detected for steps written in:

It means that scenarios displayed during execution or presented in report files would be presented in a consistent manner. If consecutive steps have the same step type, all but the first step would be renamed to start with AND.

With this behavior, the following scenario:

Runner.RunScenario(

	Given_the_user_is_about_to_login,
	Given_the_user_entered_valid_login,
	Given_the_user_entered_valid_password,
	When_the_user_clicks_login_button,
	Then_the_login_operation_should_be_successful,
	Then_a_welcome_message_containing_user_name_should_be_returned);

would result with output like:

GIVEN the user is about to login
AND the user entered valid login
AND the user entered valid password
WHEN the user clicks login button
THEN the login operation should be successful
AND a welcome message containing user name should be returned

Please note that auto-grouping would not be applied to scenarios like:

Runner.RunScenario(

	given => the_user_is_about_to_login(),
	given => the_user_entered_valid_login(),
	given => the_user_entered_valid_password(),
	when  => the_user_clicks_login_button(),
	then  => the_login_operation_should_be_successful(),
	then  => a_welcome_message_containing_user_name_should_be_returned());

as here, the user has ability to specify the step type explicitly.

Finally, it is possible to configure auto-grouping behavior on LightBDD initialization. The LightBDD configuration mechanism is described in section LightBDD Configuration. It is possible to customize auto-grouping behavior with the following code:

configuration.StepTypeConfiguration()
	.UpdatePredefinedStepTypes("given","when","then","but")
	.UpdateRepeatedStepReplacement("and also");

It is possible to disable the auto-grouping by passing empty string as RepeatedStepReplacement.

Asynchronous scenario step execution

All currently present scenario types supports asynchronous step execution. The following table summarizes the differences between synchronous and asynchronous methods:

  • void RunScenario(...)
    Runs scenario synchronously. Does not allow asynchronous steps.

    Expected step signature: void Given_...()
    Expected Runner usage: Runner.RunScenario(...)
    Availability: basic / extended scenarios

  • Task RunScenarioAsync(...)
    Runs scenario asynchronously. Expects all steps to return Task. The scenario execution has to be awaited.
    The usage example of RunScenarioAsync(...) can be found here.

    Expected step signature: Task Given_...()
    Expected Runner usage: await Runner.RunScenarioAsync(...) or return Runner.RunScenarioAsync(...)
    Availability: basic / extended scenarios

  • Task RunScenarioActionsAsync(...)
    Runs scenario asynchronously. Allows mixing of synchronous (void) and asynchronous (async void) steps. The scenario execution has to be awaited.
    The usage example of RunScenarioActionsAsync(...) can be found here.

    Expected step signature: void Given_...() or async void Given_...()
    Expected Runner usage: await Runner.RunScenarioActionsAsync(...) or return Runner.RunScenarioActionsAsync(...)
    Availability: basic / extended scenarios

Parameterized scenarios

There are use cases where given scenario has to be executed against set of different input parameters. The testing frameworks usually have a capability to parameterize test methods and declare input values with special attributes.

LightBDD preserves that capability and allows to use test framework specific mechanisms to parameterize scenarios.

Below, there are few examples of parameterized scenarios:

  • LightBDD.NUnit3
    [Scenario]
    [TestCase("EN")]
    [TestCase("PL")]
    [TestCase("DE")]
    public void Displaying_home_page_in_LANG(string lang)
    {
        Runner.RunScenario(
            _ => Given_a_customer_with_LANG_language_selected(lang),
            _ => When_the_customer_opens_the_home_page(),
            _ => Then_header_should_display_LANG_language(lang),
            _ => Then_page_title_should_be_translated(),
            _ => Then_all_menu_items_should_be_translated()
            );
    }
  • LightBDD.XUnit2
    [Scenario]
    [InlineData("EN")]
    [InlineData("PL")]
    [InlineData("DE")]
    public void Displaying_home_page_in_LANG(string lang)
    {
        Runner.RunScenario(
            _ => Given_a_customer_with_LANG_language_selected(lang),
            _ => When_the_customer_opens_the_home_page(),
            _ => Then_header_should_display_LANG_language(lang),
            _ => Then_page_title_should_be_translated(),
            _ => Then_all_menu_items_should_be_translated()
            );
    }
  • LightBDD.MsTest2
    [Scenario]
    [DataRow("EN")]
    [DataRow("PL")]
    [DataRow("DE")]
    public void Displaying_home_page_in_LANG(string lang)
    {
        Runner.RunScenario(
            _ => Given_a_customer_with_LANG_language_selected(lang),
            _ => When_the_customer_opens_the_home_page(),
            _ => Then_header_should_display_LANG_language(lang),
            _ => Then_page_title_should_be_translated(),
            _ => Then_all_menu_items_should_be_translated()
            );
    }

To learn how to parameterized scenarios are rendered, please read Formatting Parameterized Steps > Formatting Parameterized Scenarios.

Continue reading: Composite Steps Definition

Clone this wiki locally