We use cookies to provide you with a great user experience, analyze traffic and serve targeted promotions.   Learn More   Accept
Card image cap

Writing Integration Tests in ASP.NET Core using xUnit

xUnit Integration Testing  • Posted 6 months ago

Writing testable code is the norm these days, and writing tests for the components developed is a desired skill. Integration tests are one such tests which cover a bigger section of our components and let us understand how good the components work with each other. Let's discuss more about these work and how easily we can implement in dotnet core.

What are Integration Tests?

Integration testing is defined as a level of software testing wherein two or more individual components are combined and tested for expected results. These tests form the second level of the testing pyramid, which explains the different types of tests basing on their granularity and the number of tests which are needed across these levels. The other levels are Unit Tests and End-To-End tests.

Integration tests are important, because when two or more individual components are combined and tested as a whole, we can look out for any issues which might arise when the control flows across the components. They run on real resources instead of mock dependencies and since these tests involve more than one component in action, sometimes these are a bit slower when compared to the unit tests, which form the layer below these tests.

Integration tests can be written using any standard testing frameworks available such as JUnit, MSTest or xUnit. Although the syntaxes might slightly differ, all these frameworks follow the similar testing pattern, the 3As.

The 3As of software testing:

The 3As of a test program are: Arrange, Act and Assert.

Arrange the resources: whether real or mock, in this step we create the necessary dependencies and parameters which are required for our desired functionality under test to be executed. In unit tests, we create mock objects using frameworks such as Moq, wherein Integration tests we have real objects.

Act on the functionality: This step forms the actual testing, where we use the created dependencies in the previous step and call the functionality under test, the response if available from the functionality will be recorded.

Assert against the expected: This step decides whether the functionality is behaving as expected or not, by comparing the result obtained from the functionality in the previous step against a predefined expectation.

There can be two types of tests based on the return type and the way of assertion differs accordingly: there are State-based tests, return a value based on the functionality and there are Interaction-based tests which don't return a value. Example of an Interation-based test is a record creation in a database while a business logic or a mathematical logic forms a State-based test.

Setting up Integration Tests in Dotnet Core:

In ASP.NET Core, we create an integration test by starting up a test server with almost real configurations, and test any given functionality against an expected result. The integration test requires an instance of the test server and we supply to the test via constructor injection. The integration test class extends the IClassFixture interface which requires a generic type of the test server.

The testing project references the package Microsoft.AspNetCore.Mvc.Testing which provides the necessary libraries for integration testing.

Let's take a sample API method which retrieves all the readers from a ReaderStore. And we are interested in testing the functionality of the API as a whole, from an actual client point of view.

The API Class:

[Route("api/[controller]")]
[ApiController]
public class ReadersController : ControllerBase
{
	IReaderRepo repo;

	public ReadersController(IReaderRepo repo)
	{
		this.repo = repo;
	}

	[HttpGet]
	[Route("all")]
	public IEnumerable<Reader> GetAllReaders()
	{
		return this.repo.GetAllReaders();		
	}

	[HttpPost]
	[Route("add")]
	public int PostReader(Reader reader)
	{
		return this.repo.AddReader(reader);	
	}

	[HttpDelete]
	[Route("delete/{id}")]
	public void DeleteReader(int id)
	{
		this.repo.DeleteReader(id);
	}
}

and the IReaderRepo type is provided by an implementation ReaderRepo which is as follows:

public class ReaderRepo : IReaderRepo 
{

	ReaderContext ctx;

	public ReaderRepo(ReaderContext ctx)
	{
		this.ctx = ctx;
	}

	public IEnumerable<Reader> GetAllReaders() 
  {
    return this.ctx.Readers.Where(x => x.IsActive == true);
  }

	public int AddReader(Reader reader)
	{
		this.ctx.Readers.Add(reader);
		this.ctx.SaveChanges();
		return reader.Id;
	}

	public void DeleteReader(int id)
	{
		var reader = this.ctx.Readers.FirstOrDefault(x => x.Id == id);
		if(reader != null) {
			this.ctx.Readers.Remove(reader);	
			this.ctx.SaveChanges();
		}
	}
}

Now we are interested in testing the functionality of the GET Readers API "/api/readers/all" as a whole, which otherwise involves two individual units ReadersController and ReaderRepo, along with an external dependency represented by ReaderContext.

Lifesaver - The setup Class:

This is a tricky part, since in this step we define a setup class for booting up the ASP.NET test server along with the necessary settings to be passed, by overriding the default WebApplicationFactory class and providing our own implementation of the WebHostBuilder method.

public class MyReaderApiAppFactory : WebApplicationFactory<Startup>
{
	protected override IWebHostBuilder CreateWebHostBuilder()
	{
		return WebHost.CreateDefaultBuilder()
			.UseStartup<Startup>();
	}
}

This workaround is required, because the default WebApplicationFactory class if used directly is causing issues while bootstrapping the test API server. Also this is useful, since using this approach we can categorically change the appsettings we are interested in to use, by using the extension method ConfigureAppConfiguration() on top of the WebHost.CreateDefaultBuilder() method similar to setting up an actual server.

The Test Fixture class:

This forms the class which contains the integration tests we ought to be implemented for the API class.

public class MyReaderApi_ReadersController_IntegrationTests 
  : IClassFixture<MyReaderApiAppFactory>
{
	private readonly WebApplicationFactory<Startup> factory;

	public MyReaderApi_ReadersController_IntegrationTests(
      MyReaderApiAppFactory factory)
	{
		this.factory = factory;
	}

	....
}

If we observe the type which is passed along with the IClassFixture interface, it is the overridden implementation we have provided for the default WebApplicationFactory class. The constructor injection provides an instance of type MyReaderApiAppFactory class, which is assigned to its base type viz. WebApplicationFactory class.

The Integration Test:

Now let's write an integration test which retrieves all the readers available in the store and assert that a non-empty set of readers are returned.

[Fact]
public async Task GetAllReaders_WhenCalled_ReturnsNonEmptySet()
{
	/* Arrange - the server on which the API needs to be called on */
	
	// the factory instance of type WebApplicationFactory<Startup> 
  // returns a HttpClient for the test server.
	var client = factory.CreateClient();

	/* Act - invoke the API */
	var response = await client.GetAsync("/api/readers/all");

	/* Assert - the result is successful and the contents are non-empty */
	
	//Checks if the response status code is OK or not.
	response.EnsureSuccessStatusCode();	
	//Deserialize the content and check if the resultant list is non-empty
	var content = await response.Content.ReadAsStringAsync();
	var readers = JsonConvert.DeserializeObject<List<Reader>>(content);
	Assert.True(readers.Count > 0);
}

In this way, we can implement integration tests without much hassle in dotnet core.

Writing Integration Tests in ASP.NET Core using xUnit