Carl Rippon

Building SPAs

Carl Rippon
BlogBooks / CoursesAbout
This site uses cookies. Click here to find out more

Integration Testing on ASP.NET Core Web API controllers with a SQL backend

July 22, 2020
dotnet

In this post, we will test an ASP.NET Core Web API controller with a SQL backend using xUnit. All the tests will exercise the SQL database rather than mock it out. This will quickly give a good amount of coverage and confidence that all the Web API layers are working together correctly.

Controller to test

We are going to implement some tests on the following action methods within the controller.

public async Task<IEnumerable<Product>> GetAll()
{
    ...
}
public async Task<ActionResult<Product>> GetById(Guid productId)
{
  ...
}
public async Task<ActionResult<Product>> Post([FromBody] Product product)
{
  ...
}

Testing GetAll

This is our first test:

[Fact]
public async void GetAll_ReturnsTwoProducts()
{
  var configuration = GetConfig();

  var sut = new ProductsController(configuration);

  var result = await sut.GetAll();

  Assert.Equal(2, result.Count());
}

This controller has no parameters, so it’s a straightforward case of invoking the method and checking it returns the correct number of products.

The controller takes in an IConfiguration parameter which we set up using the following method:

private IConfiguration GetConfig()
{
  var builder = new ConfigurationBuilder()
    .SetBasePath(Directory.GetCurrentDirectory())
    .AddJsonFile("appsettings.json", true, true)
    .AddEnvironmentVariables();

  return builder.Build();
}

This looks at a local appsettings.json file in the test project, which contains a connection string that points to a test database.

Testing GetById

Next up is a happy path test on GetById:

[Fact]
public async void GetById_WhenKnownId_ReturnsProduct()
{
  var configuration = GetConfig();

  var sut = new ProductsController(configuration);

  var result = await sut.GetById(Guid.Parse("E897FF55-8F3D-4154-B582-8D37D116347F"));

  var okResult = Assert.IsType<OkObjectResult>(result.Result);
  var product = Assert.IsType<Product>(okResult.Value);
  Assert.Equal(Guid.Parse("E897FF55-8F3D-4154-B582-8D37D116347F"), product.ProductId);
  Assert.Equal("Chai", product.ProductName);
}

We get a known product and check it is returned.

A test for getting a product that doesn’t exist is as follows:

[Fact]
public async void GetById_WhenUnknownId_Returns404()
{
  var configuration = GetConfig();

  var sut = new ProductsController(configuration);

  var result = await sut.GetById(Guid.Parse("B051D7B5-E437-45BC-9CD1-4ED1971C2AE0"));

  Assert.IsType<NotFoundResult>(result.Result);
}

We check that an object of type NotFoundResult is returned.

Testing Post

The test on Post gets more interesting because we are now writing to the database. Here’s our first attempt:

[Fact]
public async void Post_ReturnsProduct()
{
  var configuration = GetConfig();

  var sut = new ProductsController(configuration);

  var result = await sut.Post(new Product() { ProductName = "Test" });

  var okResult = Assert.IsType<OkObjectResult>(result.Result);
  var product = Assert.IsType<Product>(okResult.Value);
  Assert.False(product.ProductId == Guid.Empty);
  Assert.Equal("Test", product.ProductName);
}

The test passes, but when the other tests are run afterward, we get the following error:

Test error

This is because, after the Post_ReturnsProduct test, there are three products in our test database. So, in the Post_ReturnsProduct test, we need to remove the test product at the end of the test … or never commit the test product to the database:

[Fact]
public async void Post_ReturnsProduct()
{
  var configuration = GetConfig();

  var sut = new ProductsController(configuration);

  ActionResult<Product> result;  using (var scope = new TransactionScope(TransactionScopeAsyncFlowOption.Enabled))  {      result = await sut.Post(new Product() { ProductName = "Test" });

      scope.Dispose();  }
  var okResult = Assert.IsType<OkObjectResult>(result.Result);
  var product = Assert.IsType<Product>(okResult.Value);
  Assert.False(product.ProductId == Guid.Empty);
  Assert.Equal("Test", product.ProductName);
}

We use TransactionScope around the controller method call so that it and the test sees the data, but the data is never committed to the database. This avoids having to manually clean up the database at the end of the test.

Neat!

The code for this post can be found in GitHub at https://github.com/carlrip/asp-net-core-testing-controllers.

Wrap up

This approach allows us to build test coverage on key paths on API controllers quickly. TransactionScope is useful when testing methods that write to the database because the test doesn’t need to do any data cleanup. It doesn’t matter whether our data layer uses EntityFramework, dapper, or something else - the approach still works nicely.

Discuss this post on twitter

ASP.NET Core 3 and React

Hands-On full stack web development using ASP.NET Core, React, and TypeScript 3

Explore the full potential and latest features of .NET Core, TypeScript 3, and React
ASP.NET Core 3 and React
Find out more