Enabling Specflow to work with our System Under Test
I have been a big champion of TDD and Unit Testing over the years but now with a lot of 3rd party libraries the question of what you are testing is becoming more and more common. There are many libraries now that do the heavy lifting for you. For example Entity Framework does the database access instead of you building your own DAO layer. So if I write a Unit Test, I shouldn’t be testing Entity Framework.
That’s when Integration Tests still appear. You often see these tests leveraging the IoC or accessing the database. They are often bundled in with the unit tests and before you know it your test libraries are a big ball of mud.
So BDD and ATDD is becoming more popular than TDD. However I find the momentum starts to slow down when writing these tests because of the effort of setting up an environment. The requirement of having a service running or a cleared down database.
However ASP.NET Core has provided many tools so you can quickly re-run your BDD tests in any environment. In this article I show you how to do this with Specflow.
Prerequisite
You have setup Visual Studio so you can use Specflow
Create a new ASP.Net Core 3 API Project
To start off I am going to create an empty shell of a project. No controllers, No Models, No Databases. We’ll drive this from our tests.
Setup a new Test App
Create a new App in your project. You will need to install it as an .net App and not .net Standard Library due to some of the dependencies you will be installing. You will need to install the following nuget packages for testing.
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.7.1" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="3.1.7" />
<PackageReference Include="SpecFlow" Version="3.4.3" />
<PackageReference Include="SpecFlow.NUnit" Version="3.4.3" />
<PackageReference Include="SpecFlow.Tools.MsBuild.Generation" Version="3.4.3" />
Code language: HTML, XML (xml)
Step 3 – Create SystemUnderTest
We need to have a way with interacting with our service. To do this we will create an interface that represents the System Under Test (SUT). At the moment it’s a Rest service so we are going to perform Post,Put, Get. However this will expand overtime.
public interface ISystemUnderTest
{
Task PostAsync<T>(string route, T entity);
HttpResponseMessage Response { get; }
Task PutAsync<T>(string route, T entity);
Task GetAsync(string route);
}
Code language: C# (cs)
Then we need to create an implementation. To do this we will create a class that uses the HttpClient. I’m not going to create this inside the class which will become clear when I initialise it later.
Also just to note, the ToJson() method is a custom Extension method I created using JSON.NET.
public class WebContext : ISystemUnderTest
{
public HttpClient Client { get; set; }
public async Task PostAsync<T>(string route, T entity)
{
Response = await Client.PostAsync(route, new StringContent(entity.ToJson(), Encoding.UTF8, "application/json"));
}
public HttpResponseMessage Response { get; private set; }
public async Task PutAsync<T>(string route, T entity)
{
Response = await Client.PutAsync(route, new StringContent(entity.ToJson(), Encoding.UTF8, "application/json"));
}
public async Task GetAsync(string route)
{
Response = await Client.GetAsync(route);
}
}
Code language: C# (cs)
Initialise our Test Server
Before we run any tests we need to startup our Rest Service to be able to test against it. We want it to load once, before all tests run. We can do this using Specflow Hooks which will give us a BeforeTestRun hook to startup the service.
The other thing we need to do is have a way of loading the ASP.Net Core service. ASP.Net Core 3 has included an In memory server for this called TestServer and has an article on how to use it using xUnit
[Binding]
public class TestServerHook
{
private static HttpClient _client;
[BeforeTestRun]
public static void BeforeTestRun()
{
var hostBuilder = Host.CreateDefaultBuilder()
.UseServiceProviderFactory(new AutofacServiceProviderFactory())
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder.UseTestServer();
webBuilder.UseStartup<Startup>();
});
var host = hostBuilder.Start();
_client = host.GetTestClient();
}
}
Code language: C# (cs)
The code should look familiar as it was in your Program.cs. The difference is where I have enabled Test Server, Started it and then created a Test Client (our HttpClient) which we will set on our SystemUnderTest.
Initialise the SUT
We now need to set the HttpClient on our SUT implementation and pass it to all our Test Step Classes. Specflow provides a simple Context Injection we can use in our Hook. Once we register ISystemUnderTest it should be passed to the constructor of any Step files.
[Binding]
public class TestServerHook
{
private readonly WebContext _context;
private static HttpClient _client;
public TestServerHook(WebContext context, IObjectContainer objectContainer)
{
_context = context;
objectContainer.RegisterInstanceAs<ISystemUnderTest>(context);
}
[BeforeTestRun(Order = 1)]
public static void BeforeTestRun()
{
var hostBuilder = Host.CreateDefaultBuilder()
.UseServiceProviderFactory(new AutofacServiceProviderFactory())
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder.UseTestServer();
webBuilder.UseStartup<Startup>();
});
var host = hostBuilder.Start();
_client = host.GetTestClient();
}
[BeforeScenario]
public void BeforeScenario()
{
_context.Client = _client;
}
}
Code language: C# (cs)
Now we are ready to test.
Create our Feature files and use the SUT
Here I am going to show a simple example to show how to use the SUT.
Firstly create your Feature file. We are going to write down what behaviour I would expect from the Rest API.
Scenario: Create a new Product
Given I have a Product called "BarrierOption"
When I create the Product
Then I received a "201" response code
And I received a valid Product
Code language: Gherkin (gherkin)
And then the Step file.
[Binding]
public class ProductSteps
{
Product Entity { get; set; }
ISystemUnderTest SUT { get; set; }
public ProductSteps(ISystemUnderTest sut)
{
SUT = sut;
}
[Given(@"I have a Product called ""(.*)""")]
public void GivenIHaveAProductCalled(string productName)
{
Entity = new Product
{
Name = productName
};
}
[When(@"I create the Product")]
public Task WhenICreateTheProduct() => SUT.PostAsync(Routes.Create<Product>(), Entity);
[Then(@"I received a ""(.*)"" response code")]
public void ThenIReceivedAResponseCode(int responseCode)
{
Assert.AreEqual(responseCode, (int)SUT.Response.StatusCode, SUT.Response.Content.ReadAsStringAsync().Result);
}
[Then(@"I received a valid Product called ""(.*)""")]
public void ThenIReceivedAValidProductCalled(string productName)
{
var entity = SUT.Response.Content.AsEntity<Product>();
Assert.AreEqual(productName, entity.Name);
Assert.Greater(entity.Id,0);
}
}
Code language: JavaScript (javascript)
On the service I would create the implementation as I implement each Step.
In order for this to truly work we need a database which we can clear down and run the test against. At the moment this assumes that my implementation has a database available. If we are to use this on our CI pipeline then it would also require us to have a database instance (and from experience this maybe a cost that the company is not willing to pay for or there is issues with connectivity from the build agent to the database server).
In Part 2 I will explore how to use Entity Framework In Memory Database for testing, removing this database dependency and making our tests quick and re-runnable.
The code to this article can be found on Github