We all know how important tests are for any application, it ensures that we build a bug free application and also helps us to avoid introducing bugs to existing functionalities as we make changes to it. In this blog, I’ll briefly go through how we can design a .NET Core API using behaviour driven development with specflow.

Before setting up automated tests, the first thing to be decided is what type of tests are required. Let’s take an example of a typical API project which in most cases will have the following modules at least

  • Controllers : Only concerned with receiving the request and sending back a response.
  • Services : Contains the main application logic
  • Repositories : Used for database access only

Now the question arises as to whether we should write unit tests for each module or integration tests or functional tests or a combination of either or all. However, there are a few things to consider like

  • Unit testing each layer is cumbersome
  • We wanted to completely avoid relying on any external data sources, instead mock them if required
  • Since most of our application logic is in the services, one option would be to test the service layer. However this may not be enough since with this approach we are not testing the end-to-end flow which involves making a request to an endpoint and validating the response.

In the end, the goal behind the decision to use BDD is to test the behaviour, which for an API can be defined as Given a request is made, we get an expected response. So what we want to do is some sort of client level testing, right from making the api request to validating the response received.

We did a bit of research and stumbled upon .NET’s own docs on Integration testing. In order to use the approach defined in the documentation, there are a few prerequisites required to set up the test project for our API, you can find them here.

The interesting aspect in this approach was how testing was done by configuring a test web host and then creating a test server client to make requests to the configured test server. So in order to set up the tests we need to do the following :

  1. Configure a test web host for our API and set up an in-memory TestServer which will expose the end points of our app.
  2. Create a test server API client, using which we will be making requests to the test server
  3. Validate the response received for every request

Now that we are clear on the approach to testing, the next question is how to achieve this with specflow. Let’s take an example of adding an endpoint in our API to create users. According to REST conventions the resource end point for this would be POST /users. On successfully creating the user the API would respond back with the created user’s Id.

Therefore the specflow scenario can be defined as follows

Scenario: Create new user
    Given I am a client
    When I make a post request to "/users" with the following data 
    "{ 'firstName' : 'Jinu','lastName':'George','emailAddress':'jinu.g@deltax.com','loginPassword':'jinu@123' }"
    Then the response status code is "200"
    And the response data should be 
    "{ id: 1 }"

On generating the steps, the step definition file would look something like this:

public class UsersResourceSteps
{
    [Given(@"I am a client")]
    public void GivenIAmAClient()
    {
        ScenarioContext.Current.Pending();
    }
    
    [When(@"I make a post request to '(.*)'with the following data '(.*)'")]
    public virtual async TaskWhenIMakeAPostRequestToWithTheFollowingata(string resourceEndPoint, string postDataJson)
    {
        ScenarioContext.Current.Pending();
    }
    [Then(@"the response status code is (.*)'")]
    public void ThenTheResponseStatusCodeI(int statusCode)
    {
        ScenarioContext.Current.Pending();
    }
    [Then(@"the response data should be (.*)'")]
    public void ThenTheResponseDataShouldB(string expectedResponse)
    {
        ScenarioContext.Current.Pending();
    }

}

The next step is to update the step definition file so that every time it is initialized we have a test server available to make requests to.

For this we need a WebApplicationFactory. By definition

A WebApplicationFactory is a factory for bootstrapping an application in memory for functional end to end tests. It needs to be passed a type parameter, which is basically a typei n the entry point assembly of the application. Typically the Startup or Program classes can be used.

The WebApplicationFactory needs to be inherited by the step definition class. As mentioned above we need to pass the entry point in our application as the type parameter to the WebApplicationFactory. Since our application here is the API, we need to pass the Startup.cs class as it is the entry point

 public class UsersResourceSteps : WebApplicationFactory<Startup>
{
    .....

However passing the Startup.cs class may not be ideal due to the following reasons:

  • The Startup class is where we register all our services needed by our API and not all of them will be required by our tests. Ideally we would like to register the services required only by the feature that is currently being tested.
  • Authentication is something that is a must for an API, but while testing it is better to by-pass the authentication and simply test the functionality

Therefore, for our tests we have to create a test startup class called TestStartup.cs in which we register only the mandatory services required by our API excluding the custom services and also we don’t configure any authentication here.

public class TestStartup
{
    public TestStartup(IConfigurationconfiguration)
    {
        Configuration = configuration;
    }
    public IConfiguration Configuration {get; }
    // This method gets called by the runtime. Use this method to addservices to the container.
    public void ConfigureService(IServiceCollection services)
    {
        services.AddMvc(.SetCompatibilityVersio(CompatibilityVersion.Version_2_1);
        // *** Do not add custom services here
    }
    // This method gets called by the runtime. Use this method to configurethe HTTP request pipeline.
    public void Configur(IApplicationBuilder app,IHostingEnvironment env)
    {
        if (env.IsDevelopment())
        {
            app.UseDeveloperExceptionPage();
        }
        else
        {
            app.UseHsts();
        }
        app.UseHttpsRedirection();
        app.UseMvc();
        // *** Do not configure Authentication here
    }
}

We can now pass this TestStartup.cs class to the WebApplicationFactory as a type parameter. With this set up, on every test run when the step definition class is instantiated we get an instance of a WebApplicationFactory through dependency injection. Using this factory we can create a test client as shown below

public class UsersResourceSteps : WebApplicationFactory<TestStartup>
{
     private WebApplicationFactory<TestStartup> _factory;
        
    private HttpClient _client { get; set; }

    public UsersResourceSteps(WebApplicationFactory<TestStartup> factory)
    {
        // instance of webapplication factory provided through Dependency Injection
        _factory = factory;
    }

    [Given(@"I am a client")]
    public void GivenIAmAClient()
    {
        // Create a test client using the factory instance
        _client = _factory.CreateClient(new WebApplicationFactoryClientOptions
        {
            BaseAddress = new Uri($"http://localhost/") // The base address of the test server is http://localhost
        });
    }
    .
    .
    .

Now that we have configured the host and we have an httpClient, we can implement our next step which is making the request to the test server using the client.

 [When(@"I make a post request to '(.*)' with the following data '(.*)'")]
        public virtual async Task WhenIMakeAPostRequestToWithTheFollowingData(string resourceEndPoint, string postDataJson)
        {
            // use the client created to make the post request
            var postRelativeUri = new Uri(resourceEndPoint, UriKind.Relative);
            var content = new StringContent(postDataJson, Encoding.UTF8, "application/json");
            Response = await _client.PostAsync(postRelativeUri, content).ConfigureAwait(false);
            // We are assigning the reponse to the global variable'Response', so that it can be validated in the next steps
        }

Validating the response is fairly straightforward, we simply assert that the response status code and the content is what is expected.

There are a couple of things to consider before we can say our test set up is complete.

Firstly, simply inheriting the WebApplicationFactory is not ideal because this means that for every test run a new web host is configured and in turn a new test server is created. What we want is to configure the web host once to create a single test server and then reuse it for all our tests. This is where Class Fixtures come into the picture.

A Class fixture is used when you want to create a single test context and share it among all the tests in the class, and have it cleaned up after all the tests in the class have finished

Therefore instead of directly inheriting the WebApplicationFactory, we inherit an IClassFixture by passing the WebApplicationFactory as the type parameter

public class UsersResourceSteps : IClassFixture<WebApplicationFactory<TestStartup>>
{
    ....
}

This will ensure that a single instance of the factory is shared across all the test runs.

Secondly, I had mentioned earlier that one of the reasons to use a TestStartup class instead of the actual Startup class is to avoid registering all the services and only register the ones required by the feature being tested. This is achieved by using WithWebHostBuilder method of the WebApplicationFactory.

It allows use to Create a new WebApplicationFactory with an IWebHostBuilder that is further customized by configuration

So for our users feature, assuming we have created a UserService , we can register it in our step definition class’s constructor as shown:

 public UsersResourceSteps(WebApplicationFactory<TestStartup> factory)
        {
            _factory = factory.WithWebHostBuilder(builder => builder.ConfigureServices(services =>
            {
                // register only the services and mocked repositories required by this feature
                services.AddScoped<IUserService, UserService>();
            }));
        }

Finally after implementing all our steps, the step definition file would look something like this

public class UsersResourceSteps : IClassFixture<WebApplicationFactory<TestStartup>>
    {
        private WebApplicationFactory<TestStartup> _factory;
        
        /// <summary>
        /// Gets or sets the http client used to make the request
        /// </summary>
        /// <value>The http client</value>
        private HttpClient _client { get; set; }

        /// <summary>
        /// Gets or sets the api response
        /// </summary>
        /// <value>The http response</value>
        protected HttpResponseMessage Response { get; set; }


        public UsersResourceSteps(WebApplicationFactory<TestStartup> factory)
        {
            //instance of WebApplicationFactory provided through Dependency Injection
            _factory = factory.WithWebHostBuilder(builder => builder.ConfigureServices(services =>
            {
                // register only the services and mocked repositories required by this feature
                services.AddScoped<IUserService, UserService>();
            }));
        }
        [Given(@"I am a client")]
        public void GivenIAmAClient()
        {
            // Create a test client using the factory instance
            _client = _factory.CreateClient(new WebApplicationFactoryClientOptions
            {
                BaseAddress = new Uri($"http://localhost/") // The base address of the test server is http://localhost
            });
        }
        
        [When(@"I make a post request to '(.*)' with the following data '(.*)'")]
        public virtual async Task WhenIMakeAPostRequestToWithTheFollowingData(string resourceEndPoint, string postDataJson)
        {
            // use the client created to make the post request
            var postRelativeUri = new Uri(resourceEndPoint, UriKind.Relative);
            var content = new StringContent(postDataJson, Encoding.UTF8, "application/json");
            Response = await _client.PostAsync(postRelativeUri, content).ConfigureAwait(false);
            // We are assigning the reponse to the global variable'Response', so that it can be validated in the next steps
        }


        [Then(@"the response status code is '(.*)'")]
        public void ThenTheResponseStatusCodeIs(int statusCode)
        {
            // validate the response status code 
            var expectedStatusCode = (HttpStatusCode)statusCode;
            Assert.Equal(expectedStatusCode, Response.StatusCode);
        }


        [Then(@"the response data should be '(.*)'")]
        public void ThenTheResponseDataShouldBe(string expectedResponse)
        {
            // validate the response content 
            var responseData = Response.Content.ReadAsStringAsync().Result;
            Assert.Equal(expectedResponse, responseData);
        }

    }

You can clone or download the complete sample API project containing the set up described above from https://github.com/Jinu1994/dotnetcore_specflow_sample

References: