Gluing together the bits of your SpecFlow test

In my previous post on SpecFlow, I showed how a single test case (Scenario in SpecFlow-speak) maps to many different bits of C# that will actually implement it.  One scenario will often map to 3 different bits – for the Given, When and Then respectively – but if you use And to extend any of the sections you will get more than 3 bits.

If the 3 or so bits can operate independently of each other, then things are pretty simple.  The problem comes if the bits need to share an understanding of the world, i.e. to share state. For instance, the scenario set-up is to get an instance of a browser to a particular web page, and then the scenario tests what happens when you enter a particular set of information via fields on that page.  Or the scenario makes a particular API call and then in a later step will test the results of that call.

If the state is managed completely for you, e.g. a stored procedure can return the last record created in a database table, then your code might not have to worry about it.

But the more common case is: the different bits need to share state, and you can’t rely on something outside your test to manage this for you.

You have 3 options.  The first is available only in simple circumstances, and the other two are always available and can be considered alternatives to each other.  The options are:

  1. Instance variables;
  2. A name / value pair store called ScenarioContext.Current;
  3. A context instance.

Instance Variables

If all the lines that make up a given scenario are always going to link to methods from the same class, and those methods are never going to be mixed with methods from other classes to implement any other scenario, then you can use instance variables.  This is the simplest approach, but it’s not always available due to the restrictions I’ve just described.

Here is a small example using instance variables, simplified for clarity.

Given the client is authorised to use the service
When the client requests a list of products
Then the server returns a list of products

[Binding]
internal class APItest
{
     APIauthorisation _authorisation;
     string _jsonResponse;

    [Given(@"^the client is authorised to use the service$")]
    internal void GetAuthorisation()
    {
        // Get authorisation and store it for later
        _authorisation = RequestAuthorisation();
        if (_authorisation == null) {Assert.Fail();}
    }

    [When(@"^the client requests a list of (\w+)$")]
    internal void RequestList(string requestedThing)
    {
        // Use the authorisation we got earlier, to get and then
        // store the list of things as Json
        _jsonResponse = SendRequest(_authorisation, requestedThing);
    }

    [Then(@"^the server returns a list of (\w+)$")]
    internal void ValidateResponse(string expectedThing)
    {
        // Check that the response we've already got is what we're expecting
        Assert.IsTrue(IsValidResponse(expectedThing, _jsonResponse));
    }
}

ScenarioContext.Current

ScenarioContext is an object that SpecFlow creates for you, and is available to your code.  ScenarioContext.Current is a Dictionary<string, object> that you can use to pass values between methods.  As this is held in the SpecFlow world, it is available across your classes, so code in class A can set a value that is read by code in class B.

So if you have the same scenario as above, but your code is split up into 3 different classes, you could do something like this:

[Binding]
internal class APIauthorisation
{
    [Given(@"^the client is authorised to use the service$")]
    internal void GetAuthorisation()
    {
        // Get authorisation and store it for later
        ScenarioContext.Current["authorisation"] = RequestAuthorisation();
        if (ScenarioContext.Current["authorisation"] == null)
        {
            Assert.Fail();
        }
    }
}

[Binding]
internal class SendRequest
{
    [When(@"^the client requests a list of (\w+)$")]
    internal void RequestList(string requestedThing)
    {
        // Use the authorisation we got earlier, to get and then
        // store the list of things as Json
        ScenarioContext.Current["jsonResponse"] =
                       SendRequest(ScenarioContext.Current["authorisation"],
                                   requestedThing);
    }
}

[Binding]
internal class CheckResponse
{
    [Then(@"^the server returns a list of (\w+)$")]
    internal void ValidateResponse(string expectedThing)
    {
        // Check that the response we've already got is what we're expecting
        Assert.IsTrue(IsValidResponse(expectedThing,
                                      ScenarioContext.Current["jsonResponse"]));
    }
}

Using ScenarioContext means that you are free to place your code wherever it makes most sense.  Note that ScenarioContext.Current is type-free – you could write an object that’s actually a Customer as the value of the string “customer”, but then read it elsewhere assuming that it’s a CustomerDetails and bad things will probably happen.

This might not be a problem for you, but if it is, you can defend yourself against it by wrapping ScenarioContext.Current in your own extension methods, that enforce a specific type for a given string.  There are some good, simple, examples on this external blog post on SpecFlow tips.

Context Instance

A context instance is an instance of a class that you have declared, that SpecFlow creates for you and then passes to whichever classes you indicate should get it.

You can declare whatever class you like, as long as its constructor takes no arguments – SpecFlow doesn’t know how to pass values to the constructor.  You can put whatever methods, properties etc. you like in the class as it is up to your code to interact with the object once it has been created for you.  You indicate which of your main, worker, classes (the ones with the [Binding] attribute) need your context class by creating a constructor for the worker class that takes the context class as an argument.

So if we take the same example as above but change it to you a context instance, we’ll get something like the code below.  It’s the same 3 classes as when we were using ScenarioContext.Current (with some modifications) plus an extra class to hold the context.

internal class Context
{
    internal APIauthorisation authorisation {get; set;}
    internal string jsonResponse {get; set;}
}

[Binding]
internal class APIauthorisation
{
    Context _context;

    internal APIauthorisation(Context context)
    {
        _context = context;
    }

    [Given(@"^the client is authorised to use the service$")]
    internal void GetAuthorisation()
    {
        // Get authorisation and store it for later
        _context.authorisation = RequestAuthorisation();
        if (_context.authorisation == null)
        {
            Assert.Fail();
        }
    }
}

[Binding]
internal class SendRequest
{
    Context _context;

    internal SendRequest(Context context)
    {
        _context = Context;
    }

    [When(@"^the client requests a list of (\w+)$")]
    internal void RequestList(string requestedThing)
    {
        // Use the authorisation we got earlier, to get and then
        // store the list of things as Json
        _context.jsonResponse = SendRequest(_context.authorisation,
                                            requestedThing);
    }
}

[Binding]
internal class CheckResponse
{
    Context _context;

    internal CheckResponse(Context context)
    {
        _context = context;
    }

    [Then(@"^the server returns a list of (\w+)$")]
    internal void ValidateResponse(string expectedThing)
    {
        // Check that the response we've already got is what we're expecting
        Assert.IsTrue(IsValidResponse(expectedThing,
                      _contextjsonResponse));
    }
}

When this test is run, SpecFlow creates an instance of Context, creates an instance of APIauthorisation and passes the Context instance to the APIauthorisation instance.  The APIauthorisation instance does things with the Context instance (in this case, sets the value of authorisation).  The same instance of Context is passed to the instance of SendRequest that SpecFlow creates, and also to the instance of CheckResponse.

The context instance has no values in it other than those put there by the worker classes.  The worker classes can access the values in the context instance through whatever mechanism you like, as in normal C#, which includes type safety.  (If you really wanted, you could design your context class so that it has a single Dictionary<string, object> and mimic the ScenarioContext.Current behaviour, but that would be weird and silly.  Don’t do that.)

You don’t have to have a single context class – you can break it down by business domain e.g. account, basket, payment etc.  Just make sure that your worker classes have constructors declared appropriately, and the right thing should happen.

 

3 thoughts on “Gluing together the bits of your SpecFlow test

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s