4R Digital Help

Automated Testing

We are very much infavour of automated testing and currently have 2 levels of Automated Tests:

  1. Unit Tests

  2. Integration Tests For more info please see below:

Unit Tests

We use unit test to test functional business, these test will often use a mocking framework (currently Moq) to mock dependencies so that we can focuss on testing the behaviour of the class / method under test. We make use of the xUnit unit testing framework for ochestrating our tests.

Integration Tests

Alongside our unit tests we also have a large number of Integration Tests, mostly for our APIs. These test make use of the Microsoft.AspNetCore.Mvc.Testing framework to spin up a test host for the API, which can then be called and tested. We also back these tests with a Sql Database with is hosted in Docker and has the migrations applied. Each test is responsible for creating its own initial data in the database and we use the Respawn library to reset the DB after each test.

Our implementation

NOTE: Before you start, you will need to run an instance of SQL in Docker. You can use the sql-server-docker-compose.yml file for this.

If you look at the FourDBS.UnitTests project in the Api\Framework folder you will see the following files:

  • ApiBaseTest - A base class for all Database backed integration tests (typically API tests). It contains functions for initialising the DB and has helper properties for accesing the DB.

  • ApiCollection - An xUnit Collection class which is used on the ApiBaseTest class above.

  • ApiOrDbBackedTestApplication - This implements microsoft's ApiOrDbBackedTestApplication class which is part of the Microsoft.AspNetCore.Mvc.Testing library, and overiddes the WebHost program class. This class initialises the SQL DB, creating it if necessary and running, allows the overriding of configured services and config, essentially it allows you to hook into various methods in the aspnet start up pipeline.

  • DataInitialisationHelper - A helper class that we have created to help with resetting the Database between tests and executing migrations.

  • HttpClientHelper - A helper class for calling API's in the various tests and deserializing results.

  • TestConnectionStringProvider - in our code we use a connection string provider to get a connection string for Entity Framework and/or NPoco. This is an implemenation the we use to overried the standard provider, so that the data access code points at the test DB.

  • TestDbHelper - Additional functionality for creating / drop / checking availability for the test database.

Creating and API or DB Backed Integration Test

To creat an integration test, use the following syntax:

[Trait("Category", "AccountsApiTests")] public class MyApiTests : ApiBaseTest { public MyApiTests( ApiOrDbBackedTestApplication factory, DataInitialisationHelper fixture, ITestOutputHelper testOutputHelper) : base(factory, fixture, testOutputHelper, "MyApi") { } [Fact] public async Task GetSometing() { var testHttpClient = await InitialiseAndGetClientAsync(); // Get an EF context await using var context = GetContext(); // Add some data to the DB var myEntity = new MyEntity(); context.MyEntities.Add(myEntity); await context.SaveChangesAsync(); // ------------------ // Call the API var url = $"{_apiEndpoint}/{account.Id}"; var response = await testHttpClient.CallGet(url); response.EnsureSuccessStatusCode(); Assert.Equal(...); }

Further reading

For more info see:

For the future

Testing tips

Below are some tips for unit testing with xUnit.

Using Theories

As specified in our best practice document, using Theories can simplify our tests and save lines of code. For more information on using Theories, see: xUnit Theory: Working With InlineData, MemberData, ClassData.

One short coming of Theories is that it can lead to methods with larger parameter lists, e.g.

[Theory] [InlineData(1,2,3,4,5,6,7,8,9,....)] public void TheoryTest(int param1, int param2, int param3, int param4, int param5, int param6, int param7, int param8, int param9, ...) { ... }

The good news is that there is a way to pass complex types into Theories (though you will struggle to find it documented). Using this technique, the above method could be refactored into:

public class TestData { public record TestScenarioDto { public int Param1 { get; set; } . . } public static IEnumerable<object[]> TestScenario1() { yield return [ new TestScenarioDto { Param1 = 123, .... }]; } } [Theory] [MemberData(nameof(TestData.TestScenario1), MemberType = typeof(TestData))] public void TheoryTest(TestScenarioDto testScenario) { . . . }
Last modified: 03 June 2024