Skip to content

Writing scenarios

Michael W Powell edited this page Dec 8, 2020 · 8 revisions

The vocabulary of an xWellBehaved.net scenario is very similar to a Cucumber scenario. Their anatomy, however, is rather different. An xWellBehaved.net scenario is contained completely in code.

The basic anatomy of an xWellBehaved.net scenario is:

  • A containing assembly
  • A containing type (e.g. C# class)
  • A scenario (e.g. C# method)
  • Some steps (e.g. delegates defined inside a C# method)

A containing assembly contains the tests for the features or specs of your system under test (SUT). For example, if the library you want to test is named Widgets, you might create a library named Widgets.Features or Widget.Specs (see What kind of tests can I write using xWellBehaved.net). This is the equivalent of creating a test library using xUnit.net or NUnit.

A containing type groups your scenarios together. For example, if one of your features is a calculator, you might create a class named CalculatorFeature. This is the equivalent of creating a test class to contain a set of xUnit.net facts or NUnit tests.

The name of your scenario should describe it adequately. For example, if you want to cover the scenario of adding numbers together with your calculator then you might name the scenario Addition.

The steps within a scenario contain the code which is executed during the scenario and are typically defined using the vocabulary of Gherkin.

Example

namespace Widgets
{
    public class Calculator
    {
        public int Add(int x, int y) => x + y;
    }
}
namespace Widgets.Features
{
    using Xunit;
    using Xwellbehaved;

    // In order to build complicated widgets
    // As a widget builder
    // I want a calculator for performing basic arithmetic
    public class DebuggingScenarios
    {
        [Scenario]
        public void Addition(int x, int y, int expected, Calculator calculator, int actual)
        {
            // NOTE: This is a simple example. You provide a simple `Action`
            // to the framework, and the framework wires it up for you. All
            // the usual language rules apply as method and class complexity
            // increase. That is entirely up to you to manage that complexity.

            "Given the number 1".x(() => x = 1);

            "And the number 2".x(() => y = 2);

            "And the expected 3".x(() => expected = 3);

            "And a calculator".x(() => calculator = new Calculator());

            "When I add the numbers together".x(() => actual = calculator.AssertNotNull().Add(x, y));

            "Then the answer is 3".x(() => actual.AssertEqual(expected));
        }
    }
}

Consider this scenario bit by bit.

Feature Introduction

// In order to build complicated widgets
// As a widget builder
// I want a calculator for performing basic arithmetic
public class DebuggingScenarios { ... }
//           ^^^^^^^^^^^^^^^^^^

As in Cucumber, this is free text which is not parsed. Its purpose is to describe the feature to the reader. The name of the feature is the class name, which is the equivalent of the first line of a Cucumber feature introduction.

Scenario Name

    [Scenario]
//  ^^^^^^^^^^
    public void Addition(int x, int y, int expected, Calculator calculator, int actual) { ... }

The [Scenario] attribute marks the method as a scenario and is the equivalent of the xUnit.net [Fact] and NUnit [Test] attributes. The name of the scenario is the method name.

Parameters

The parameters of the scenario method are simply a convenient way of declaring the variables which will be used within the scenario steps. The method will be invoked with default values passed for each parameter, i.e. null for reference types and zero values for value types. You can also supply example values for these parameters for executing scenarios in a data-driven/spec by example manner (see Debugging Scenarios with examples). If you do not want to declare your variables as parameters in this way, you can declare them within the scenario body instead.

[Scenario]
public void Addition()
{
    var x = 0;
    //  ^
    var y = 0;
    //  ^
    var expected = 0;
    //  ^^^^^^^^
    Calculator calculator = null;
    //         ^^^^^^^^^^
    var actual = 0;
    //  ^^^^^^
    ...
}

Which is actually closer to being accurate when you write this:

    int x = default;
//  ^^^     ^^^^^^^
    int y = default;
//  ^^^     ^^^^^^^
    int expected = default;
//  ^^^            ^^^^^^^
    Calculator calculator = default;
//  ^^^^^^^^^^              ^^^^^^^
    int actual = default;
//  ^^^          ^^^^^^^

Steps

Lastly are the steps themselves. In this example, they are directly equivalent to Cucumber steps. Like Cucumber, xWellBehaved.net does not distinguish between each type of step. It is up to you to organize them as you see fit. You do not have to use the Given, When, Then language. The names of the steps themselves are free text and you can choose to use any convention you like (see Step names). If you do not like the x method you can extend xWellBehaved.net with your own methods (see Extending xWellBehaved.net).

Warning: Complexity is the name of the game, or, rather, managing that complexity. The key to writing good xWellBehaved.net based unit test code is to recognize your step entry point as the launching point for any of your scenario test steps. What do we mean by that?

"This is your launching point, or, rather, the System.Action callback there in".x(() => { ... });
//                                                                                ^^^^^^^^^^^^^

All the usual language rules apply. So we could just as easily write something like the following.

void OnAnotherLaunchingPoint() { ... }
//   ^^^^^^^^^^^^^^^^^^^^^^^^^
"Another launching point".x(OnAnotherLaunchingPoint);
//                          ^^^^^^^^^^^^^^^^^^^^^^^

Accessing code apart from these entry points is dangerous and you will probably sustain access violations or similar rude behavior from the run time environment upon execution.

Side note: This guidance is especially accurate when accessing xUnit.net furnished resources such as ITestOutputHelper. It is critical that you do so within the context of the xWellBehaved.net entry points.

We are open to receiving feedback that enhances the overall end user experience using our framework, of course. Feel free to also stop by and offer us a handshake on Gitter.

For an explanation of what happens when you run a scenario, see Running scenarios.

Clone this wiki locally