End to End Testing of Web API 2 using OWIN and Specflow

One of the problem of end to end testing of Web API is that we need to deploy the API first and then run the tests. That means we are deploying without checking whether we did break something or not and find out the issue after deployment. To solve this problem a common practice is to setup the CI server (TeamCity / Jenkins etc) in a way so that the process deploy the API in an intermediate test server and then deploy to target server once tests passed in test server.

Another problem is isolate some dependencies that make the tests fragile as you can’t mock those dependencies. Suppose you have an API that returns list of books. The API also provides best deals available for each books. The best deals loaded from another API. Now if we write some tests to make sure each books loaded correct deals then that tests will break when data changed in the deal provider API. Even though main point of end to end testing is the test the whole workflow, but there will be some situations where isolate/mock external api is necessary. Most probably having no end to end testing is better than having fragile end to end tests or tests that don’t provide you any confidence in result.

Web API 2 using OWIN can solve these two important problems of end to end testing. On top of these, your tests will be faster and debugging will be easier as you don’t need to attach the project to iis/iisexpress process before run the tests. You just need run your tests in debug mode to debug the Web API. Lets see the idea in action. The code I am showing here using ASP.NET WebAPI and Specflow for testing the API. I am also using Ninject as DI container for API and NSubstitute to mock dependencies.

Create a webapi2 project using visual studio. Here is the code of owin Startup file.

using System;
using System.Collections.Generic;
using System.Diagnostics.Contracts;
using System.Reflection;
using System.Web.Http;
using System.Web.Http.Dependencies;
using Microsoft.Owin;
using Ninject;
using Ninject.Syntax;
using Owin;

[assembly: OwinStartup(typeof(BookApi.Startup))]

namespace BookApi
{
    public class Startup
    {
        public static IKernel Kernel { get; set; }

        public void Configuration(IAppBuilder app)
        {
            var config = new HttpConfiguration();

            config.DependencyResolver = new NinjectDependencyResolver(CreateKernel());

            config.MapHttpAttributeRoutes();

            app.UseWebApi(config);
        }

        private IKernel CreateKernel()
        {
            Kernel = new StandardKernel();
            Kernel.Load(Assembly.GetExecutingAssembly());
            return Kernel;
        }
    }
}

Now write the controller that returns all available books.


using System.Web.Http;
using BookApi.Services;

namespace BookApi.Controllers
{
    [RoutePrefix("books")]
    public class BooksController : ApiController
    {
        private readonly IBooksService _booksService;

        public BooksController(IBooksService booksService)
        {
            _booksService = booksService;
        }

        [Route]
        public IHttpActionResult Get()
        {
            return Ok(_booksService.GetAll());
        }
    }
}

The controller using BooksService class and here is the code for BooksService class.


public class BooksService : IBooksService
{
    private readonly IBooksRepository _repository;
    private readonly IDealsService _dealsService;

    public BooksService(IBooksRepository repository, IDealsService dealsService)
    {
        _repository = repository;
        _dealsService = dealsService;
    }

    public IEnumerable<Book> GetAll()
    {
        var books = _repository.GetAll().ToList();
        var deals = _dealsService.GetByBookIds(books.Select(x => x.Id));

        foreach (var book in books)
        {
            if (deals.ContainsKey(book.Id))
            {
                book.AvailableDeals = deals[book.Id];
            }
            else
            {
                book.AvailableDeals = Enumerable.Empty<Deal>();
            }
        }

        return books;
    }
}

The BooksService class loads all available books from database using BooksRepository class. After loading all books the class using DealsService class to get available deals for all books and add them for each book. DealsService class actually just call another API to load all leads based on supplied book ids.

Now take a look at our test feature file in specflow.

specflow-owin-figure-2

We will create binding class that load the book api app in memory before a feature test start and dispose when feature tests finished.

[Binding]
public class WebServer
{
    [BeforeFeature]
    public void CreateServer()
    {
        FeatureContext.Current.TestServer(TestServer.Create<Startup>());
    }

    [AfterFeature]
    public void StopServer()
    {
        var server = FeatureContext.Current.TestServer();
        server.Dispose();
    }
}

public static class FeatureContextExtensions
{
    private const string KeyServer = "server";

    public static TestServer TestServer(this FeatureContext source)
    {
        return source.Get<TestServer>(KeyServer);
    }

    public static void TestServer(this FeatureContext source, TestServer server)
    {
        source.Add(KeyServer, server);
    }
}

Simple but most interesting code here is “TestServer.Create()”. TestServer class is from nuget package “Microsoft.Owin.Testing”. So you need add this nuget package in your test project. The “Startup” is owin startup class inside web api.

Now lets take a look at the step definition and how we use test server to send get request to web api and mock external api DealsServer.


[Binding]
[Binding]
public class GetBooksSteps
{
    [When(@"I requested to get all books")]
    public void GivenIRequestedToGetAllBooks()
    {
        var mockDealService = NSubstitute.Substitute.For<IDealsService>();
        mockDealService.GetByBookIds(Arg.Any<IEnumerable<int>>()).Returns(
            new Dictionary<int, List<Deal>>
            {
                {1, new List<Deal>()
                {
                    new Deal(), // 2 deals for book id 1
                    new Deal()
                }},
                {2, new List<Deal>
                {
                    new Deal() // 1 deal for book id 2
                }}
            });

        Startup.Kernel.Rebind<IDealsService>().ToMethod(x => mockDealService);

        using (var response = FeatureContext.Current.TestServer().HttpClient.GetAsync("/books").Result)
        {
            using (var httpContent = response.Content)
            {
                var content = httpContent.ReadAsStringAsync().Result;

                var books = JsonConvert.DeserializeObject<IEnumerable<Book>>(content);

                ScenarioContext.Current.Add("books", books);
            }
        }
    }

    [Then(@"I should get following books")]
    public void ThenIShouldGetFollowingBooks(Table table)
    {
        var expectedBooks = table.CreateSet<BookRow>();
        var books = ScenarioContext.Current.Get<IEnumerable<Book>>("books");

        books.ShouldNotBeNull();
        books.Count().ShouldEqual(expectedBooks.Count());
        books.All(x => expectedBooks.Any(eb => eb.Id == x.Id && eb.Title == x.Title && eb.DealsCount == x.AvailableDeals.Count())).ShouldBeTrue();
    }

    public class BookRow
    {
        public int Id { get; set; }
        public string Title { get; set; }
        public int DealsCount { get; set; }
    }

}

As you can see that I am mocking the DealerService class using NSubstitute here so my tests does not break when number of deals changed for books in the api that provide me book deals. This is now possible because I am running my webapi in same process of test runner. Here we are using httpclient to call the Api request url “/books” to get available books with deals.

That’s all for today. Happy coding 🙂

Advertisements

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 )

Twitter picture

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

Facebook photo

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

Google+ photo

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

Connecting to %s