laboratory test tubes
.NET

OWIN WebAPI integration testing with Entity Framework Core and Castle Windsor

Building WebAPIs using an ‘integration-test-first-approach’ can save you a lot of time. Letting your tests do the heavy lifting will surely pay out almost instantly. Used together with automated concurrent testing tools like nCrunch or Live Unit Testing in Visual Studio, you will get instant feedback and test your API from top to bottom over and over again.

The moving parts

To pull this off, we primarily rely on the Microsoft.Owin.Testing package. In this example I’m using Castle Windsor to handle IoC. There are numerous ways of writing tests using the OWIN Testserver, and this is just one of them. Hold on tight, and I’ll try to guide you through my take on this.

You can find the complete example project on GitHub, so I will only point of what I recon to be the most relevant pieces.

Off we go!

In the OWIN startup class, normally Startup.cs , I usually make a virtual method which in turn is overridden in my test startup class, TestStartup.cs. I find this to be a clean way to distinguish deviations from the normal startup.

Virtual sanity in Startup.cs

protected virtual IWindsorContainer Bootstrap()
{
    var container = new WindsorContainer();

    container.Register(Classes.FromThisAssembly().BasedOn<ApiController>).LifestyleTransient());

    var options = new DbContextOptionsBuilder<MyContext>).UseSqlServer("MyConnection").Options;

    container.Register(Component.For<MyContext>().UsingFactoryMethod(c => new MyContext(options)).LifestyleTransient());

    return container;
}

The virtual method bootstraps the application. This can be done using installers or in whatever way one would find preferable. The key takeaway here is that we handle the registration of the EF-context ‘by hand’ by feeding the factory method the options which we create in that same virtual method.

The TestStartup-class

The test startup-class which is used for testing and fed to the OWIN TestServer, overrides the virtual method in order to set up EF Core and use its in-memory database. It also resolves the context after it’s registered. Doing this in Startup.cs is of course not something we would like to do – but in TestStartup.cs, we do this in order to set the static variable that will hold our resolved context.

public class TestStartup : Startup
{
  public static MyContext MyContext { get; set; }
  
  protected override IWindsorContainer Bootstrap()
  {
    var container = new WindsorContainer();

    container.Register(Component.For<ValuesController>().ImplementedBy<ValuesController>().LifestyleTransient());

    var options = new DbContextOptionsBuilder<MyContext>().UseInMemoryDatabase("MyConnection").Options;

    container.Register(Component.For<MyContext>().UsingFactoryMethod(c => new MyContext(options)).LifestyleTransient());

    MyContext = container.Resolve<MyContext>();
    return container;
  }
}

Static to the rescue!

After having obtained a reference to our context outside of the OWIN TestServer, enables us to modify the context and prepare in-memory test data when executing our integration tests.

[TestClass]
public class ValuesControllerTest
{
    private TestServer TestServer { get; set; }
    private static MyContext MyContext { get; set; }

    [TestInitialize]
    public void Init()
    {
        TestServer = TestServer.Create<TestStartup>();
        TestServer.BaseAddress = new Uri("http://localhost");
        MyContext = TestStartup.MyContext;
    }

    [TestMethod]
    public async Task Get_WhenValueExists_200()
    {
        //arrange
        MyContext.Values.Add(new Value { Id = 1, Epicness = 1337 });
        await MyContext.SaveChangesAsync();

        //act
        var result = await TestServer.CreateRequest("values/1").GetAsync();

        //assert
        result.EnsureSuccessStatusCode();
        var deserializedResult = JsonConvert.DeserializeObject<Value>(await result.Content.ReadAsStringAsync());
        Assert.AreEqual(1, deserializedResult.Id);
        Assert.AreEqual(1337, deserializedResult.Epicness);
    }
}

It’s a wrap!

Grab the code on GitHub, and take it for a spin!

Leave a Reply

Your email address will not be published. Required fields are marked *