Using xUnit fixtures with SpecFlow

It can be a challenge to implement new frameworks or tools in your daily life. Often it takes time to find out the quirks and best practices which can take time and even lead to doubts and abandoning frameworks. I have been using SpecFlow for a while now and I keep finding new ways to tackle issues and get cleaner code behind the specs (I am still having trouble creating clean & clear functional requirements though).

One of the things I keep bumbing into is using settings and sharing code between different specs. In the following example I will explain my latest find: using xUnit fixtures to get test settings and reusable api clients for multiple tests.

The scenario

For this post I am implementing the following specification:

Feature: Search Breweries
	As a beer lover
	I want to search for breweries
	So I can plan a visit to a brewery
	
Scenario: Searching for breweries nearby
	Given my current location is "51.6582661,5.279888"
	When searching for breweries closeby
	Then a list of breweries should be returned

Just a straightforward scenario: a call to an api with some result.

A possible implementation

Below is a basic implementation. The SettingsHelper would be an uggly helper just to read the settings, in every specification I have to create the rest client again:

[Binding]
public class SearchBreweries
{
    private const string Location = "LOCATION";
    private const string Breweries = "BREWERIES";

    private readonly ScenarioContext _scenarioContext;

    public SearchBreweries(ScenarioContext scenarioContext) 
    {
	    _scenarioContext = scenarioContext;
    }

    [Given("my current location is \"(.*)\"")]
    public void GivenMyCurrentLocation(string location) =>
	    _scenarioContext.Add(Location, location);

    [When("searching for breweries closeby")]
    public async Task WhenSearchingForBreweriesCloseby()
    {
	    var api = Refit.RestService.For<IOpenBreweryDbApi>("https://api.openbrewerydb.org");
	    var breweries = await api.SearchBreweriesByLocation(_scenarioContext.Get<string>(Location));
	    _scenarioContext.Add(Breweries, breweries);
    }

    [Then("a list of breweries should be returned")]
    public void ThenAListOfBreweriesShouldBeReturned()
    {
	    var breweries = _scenarioContext.Get<IEnumerable<Brewery>>(Breweries);
	    Assert.NotEmpty(breweries);
    }
}

But: what if I want to test on different environments? Then the base address to the API will change. And what if I have different features for different API calls? Do I want to repeat myself and construct a rest service in every test?

xUnit Fixtures to the rescue!

There are different ways to share context within tests by using fixtures in xUnit. Please read all the details in the xUnit documentation if you want to know all the details.

For my scenario I am going to use two different types of fixtures: a class fixture and a collection fixture.

The class fixture to share the code for the rest service

The implementation of the RefitFixture is simple. Maybe too simple but in a real world you might need to add more parameters for custom headers, authentication, etc.

public class RefitFixture<TRefitApi> : IDisposable
{
    public TRefitApi GetRestClient(string baseAddress) =>
	    Refit.RestService.For<TRefitApi>(baseAddress);

    public void Dispose()
    {
    }
}

To use the fixture you need to add the fixture to the test class:

[Binding]
public class SearchBreweries : RefitFixture<IOpenBreweryDbApi>
{
    private const string Location = "LOCATION";
    private const string Breweries = "BREWERIES";

    private readonly ScenarioContext _scenarioContext;
    private readonly RefitFixture<IOpenBreweryDbApi> _refitFixture;

    public SearchBreweries(ScenarioContext scenarioContext, RefitFixture<IOpenBreweryDbApi> refitFixture)
    {
	    _scenarioContext = scenarioContext;
	    _refitFixture = refitFixture;
    }

    [Given("my current location is \"(.*)\"")]
    public void GivenMyCurrentLocation(string location) =>
	    _scenarioContext.Add(Location, location);

    [When("searching for breweries closeby")]
    public async Task WhenSearchingForBreweriesCloseby()
    {
	    var breweries = await _refitFixture
		    .GetRestClient("https://api.openbrewerydb.org")
		    .SearchBreweriesByLocation(_scenarioContext.Get<string>(Location));
	    _scenarioContext.Add(Breweries, breweries);
    }

    [Then("a list of breweries should be returned")]
    public void ThenAListOfBreweriesShouldBeReturned()
    {
	    var breweries = _scenarioContext.Get<IEnumerable<Brewery>>(Breweries);
	    Assert.NotEmpty(breweries);
    }
}

The collection fixture to share settings for all features

In the class above I have a hard coded base address for the API. I want to make this URL configurable so I can test with different environments, preferrably from a CI/CD pipeline with environmental variables. The settings will not change so I can use a collection fixture for all specifications. A collection specification needs two classes:

[CollectionDefinition("Settings collection")]
public class SettingsCollection : ICollectionFixture<SettingsFixture>
{
	// This class has no code, and is never created. Its purpose is simply
	// to be the place to apply [CollectionDefinition] and all the
	// ICollectionFixture<> interfaces.
	// READ MORE: https://xunit.net/docs/shared-context
}
public class SettingsFixture : IDisposable
{
    public readonly AppSettings AppSettings;

    public SettingsFixture()
    {
	    var configuration = new ConfigurationBuilder()
		    .AddJsonFile("appsettings.local.json", optional: true)
		    .AddEnvironmentVariables()
		    .Build();

	    AppSettings = new AppSettings
	    {
		    BaseAddress = configuration["AppSettings:BaseAddress"]
	    };
    }

    public void Dispose()
    {
    }
}

To use the collection fixture I need to add an attribute to my class and inject the fixture:

[Binding]
[Collection("Settings collection")]
public class SearchBreweries : RefitFixture<IOpenBreweryDbSearchApi>
{
    private const string Location = "LOCATION";
    private const string Breweries = "BREWERIES";

    private readonly ScenarioContext _scenarioContext;
    private readonly RefitFixture<IOpenBreweryDbSearchApi> _refitFixture;
    private readonly SettingsFixture _settingsFixture;

    public SearchBreweries(
	    ScenarioContext scenarioContext, 
	    RefitFixture<IOpenBreweryDbSearchApi> refitFixture,
	    SettingsFixture settingsFixture)
    {
	    _scenarioContext = scenarioContext;
	    _refitFixture = refitFixture;
	    _settingsFixture = settingsFixture;
    }

    [Given("my current location is \"(.*)\"")]
    public void GivenMyCurrentLocation(string location) =>
	    _scenarioContext.Add(Location, location);

    [When("searching for breweries closeby")]
    public async Task WhenSearchingForBreweriesCloseby()
    {
	    var breweries = await _refitFixture
		    .GetRestClient(_settingsFixture.AppSettings.BaseAddress)
		    .SearchBreweriesByLocation(_scenarioContext.Get<string>(Location));
	    _scenarioContext.Add(Breweries, breweries);
    }

    [Then("a list of breweries should be returned")]
    public void ThenAListOfBreweriesShouldBeReturned()
    {
	    var breweries = _scenarioContext.Get<IEnumerable<Brewery>>(Breweries);
	    Assert.NotEmpty(breweries);
    }
}

Done! Two reusable fixtures to make my tests much cleaner. Especially when multiple different variables and API calls are tested.

Thats it. Just a quick write-up to share some tips about SpecFlow in real life. If you have any comments, tips or questions please do not hesitate to write a comment below! Also, I created a small demo project which you can find on GitHub.