Component Tests With Collection & Class Fixtures

A component test verifies the functionality of individual components within an application by isolating and testing them in a controlled environment.

Common techniques used in C# component tests are:

Component tests dont crossing unit boundaries

Component Test example

I’ve worked in teams where Integration Tests are re-used, the external dependencies are mocked out with another appsettings file. This is in my opinion then a Component Test, so the two resources above HTTP and Database are mocked / stubbed.

The appsettings file could then be called something like appsettings.Mock.json

Using a fixture

It makes sense to break the tests down but still group them by Features - these should be already grouped by your controllers. By design controllers are allowed to do too much, you can add as many endpoints in them as you like - thats a different topic, see MVC Controllers are Dinosaurs - Embrace API Endpoints. For my features examples however grouping responsability makes component testing by feature easier.

Consider these controllers, Artists has simple CRUD operations and so does Songs, these operations are abstracted away and then injected as an interface, using the Controllers folder is simply following convention over configuration.

I would understand this as one Artist has many Songs.

1
2
Foo.Api/Controllers/ArtistsController.cs
Foo.Api/Controllers/SongsController.cs

The component tests could then be created as Features/Artists and Features/Songs, understandably there could be overlap if a Artist is persisted with some songs - ideally the link would just be the foreign key and not the whole implementation so design considerations need to thought out.

1
2
Foo.Api.ComponentTests/Features/Artists/
Foo.Api.ComponentTests/Features/Songs/

The abstractions the controller instantiates though dependency injection would still be injected by the applications framework, their behavior also doesnt change however the configuration (appsettings in .Net) can be changed by environment. This means the application needs to have the same appsettings values as the component tests.

Collection Fixtures

When to use: when you want to create a single test context and share it among tests in SEVERAL TEST CLASSES, and have it cleaned up after all the tests in the test classes have finished. - xunit.net/docs/shared-context

Examples of data it could share

  • Options values (config)
  • Clients (classes that use HttpClient to call APIs)

The tests can use something called a Fixture to instantiate their dependencys. One or more fixtures are then injected into our tests. So below the CollectionFixture is really a base class to share context between tests.

Example Foo.Api.ComponentTests/Fixtures/CollectionFixture.cs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
/// <summary>
/// A base class for any tests that want to implement xunit's ICollectionFixture functionality to share context between tests.
/// Test classes that use this fixture should be decorated with [Collection("Shared Collection Fixture")]
/// </summary>
public class CollectionFixture : IAsyncLifetime
{
private FooServiceClient _client;
private readonly FooBarOptions _fooBar;

public CollectionFixture()
{
var appSettings = "appsettings.Mock.json";

if (Environment.GetEnvironmentVariable("DOTNET_ENVIRONMENT") == "Uat")
appSettings = "appsettings.Uat.json";

var configuration = new ConfigurationBuilder()
.AddJsonFile(appSettings)
.Build();

_fooBar = configuration
.GetSection(FooBarOptions.FooBar)
.Get<List<FooBarOptions>>();
}

public FooServiceClient FooServiceClient
{
get
{
if (_client != null)
return _client;

_client = new FooService(_fooBar);
return _client;
}
}

// ... DisposeAsync, InitializeAsync
}

We use IAsyncLifetime to call into the services to dispose of resources. If you use InitializeAsync here to seed any data you must remember its for all tests. If you want to seed data just for a subset then see Class Fixtures below.

1
2
3
4
5
6
7
8
9
10
public Task DisposeAsync()
{
// clean up test data if the database was not brought up as part of the environment, ie: its not in a test container

// dispose of clients
_client?.Dispose();
return Task.CompletedTask;
}

public Task InitializeAsync() => Task.CompletedTask;

To then allow the CollectionFixture to be instanciated and managed we need to create a SharedCollectionFixture

1
2
3
4
5
6
7
[CollectionDefinition("Shared Collection Fixture")]
public class SharedCollectionFixture : ICollectionFixture<CollectionFixture>
{
// This class has no implementation as is never created.
// Its purpose is simply to be the place to apply [CollectionDefinition] and all the ICollectionFixture<> interfaces.
// See https://xunit.net/docs/shared-context
}

Test classes that want to use CollectionFixture should be decorated with [Collection("Shared Collection Fixture")]. Their constructor parameters can then include CollectionFixture collectionFixture. The facts can then access _collectionFixture.FooServiceClient

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
[Collection("Shared Collection Fixture")]
public class ArtistTests
{
private readonly CollectionFixture _collectionFixture;

public ArtistTests(CollectionFixture collectionFixture)
{
_collectionFixture = collectionFixture;
}

[Fact]
public async Task U_I_E()
{
// Arrange
// Act
// Assert
}
}

FooServiceClient

The FooServiceClient should inherit/implement IDisposable to dispose of any clients it may have. This is called from the fixtures DisposeAsync method to ensure all state is disposed of.

GC.SuppressFinalize(this) will prevent derived types that introduce a finalizer from needing to re-implement IDisposable to call it. See CA1816: Call GC.SuppressFinalize correctly

Alternatively the client FooServiceClient could use the sealed keyword to prevent inheritance.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class FooServiceClient : IDisposable
{
private readonly HttpClient _client;

public FooServiceClient(FooServiceOptions fooServiceOptions)
{
_client = new HttpClient { BaseAddress = new Uri(fooServiceOptions.Url) };
}

public async Task<ArtistDto> GetArtistAsync(Guid artistId)
{
// ...
}

public void Dispose()
{
_client.Dispose();
GC.SuppressFinalize(this);
}
}

Class Fixtures

When to use: when you want to create a single test context and share it among all the tests ONE TEST CLASS, and have it cleaned up after all the tests in the class have finished. - xunit.net/docs/shared-context

Examples of what it can do

  • Seed data before each test, ie: call your API under test and seed data (this is more for an integration tests, it may not be sensible for component but I kept this point for completeness should you be reusing component as integration tests)
  • Provide IDs (often GUIDs) for the seeded data, you would then use this in your tests

Create the fixture the same as above but it will not need the CollectionDefinition class.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
/// <summary>
/// A base class for any tests that want to implement xunit's IClassFixture functionality to share context between tests.
/// This class should be inherited by a child class to be used with IClassFixture<T>
/// </summary>
public class FindStuffFixture : IAsyncLifetime
{
// ... private state

//Note that if you wanted to use the class fixture that provides for example test clients to do http calls you would just as for it in the class fixtures constructor
public FindStuffFixture(CollectionFixture collectionFixture)
{
// ... set private state
}

public async Task InitializeAsync()
{
// ... seed data using `collectionFixture` clients

var tasks = new List<Task>
{
// ... collection of tasks to run in parallel
};
await Task.WhenAll(tasks);

// ... its sensible to still wait a 1 to 10 seconds
await Task.Delay(1000);
}

public Task DisposeAsync()
{
// ... cleanup if needed

// then return
return Task.CompletedTask;
}
}

Create the tests and inherit/implement IClassFixture<T> and IAsyncLifetime.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class FindStuffTests : IClassFixture<FindStuffFixture>, IAsyncLifetime
{
public FindStuffTests(FindStuffFixture findStuffFixture)
{
_findStuffFixture = findStuffFixture;
}

[Fact]
public async Task U_I_E()
{
// Arrange
// Act
// Assert
}
}

Using Collection Fixtures And Class Fixtures together

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
[Collection("Shared Collection Fixture")]
public class FindStuffTests : IClassFixture<FindStuffFixture>, IAsyncLifetime
{
public FindStuffTests(CollectionFixture collectionFixture, FindStuffFixture findStuffFixture)
{
_findStuffFixture = findStuffFixture;
_collectionFixture = collectionFixture;
}

[Fact]
public async Task U_I_E()
{
// Arrange
// Act
// Assert
}
}

References