The TestApi pattern helps decouple tests from our system and enables more stable and reusable tests.
I’ve shown how I use the pattern in F#. Now here’s a similar example in C#.
Suppose you’re following SOLID at the architectural level. This means each service pushes it’s dependencies behind abstractions that it owns and some later caller injects the concrete implementations.
The calling service likely expects all dependency implementations to behave consistently from the perspective of the service. This includes queries and any side-effects the service would be effected by.
TestApi allows us to write those shared behavior expectations once and verify them across many implementations or configurations.
One can imagine how some of the service dependencies could vary. For example, chat might have a stand-alone user store or it might want to share a user store with the hosting application. We might want to store file attachments in blob storage, or publish them to a CDN for fast delivery, or leverage some unified user-centric media service.
These are valid use cases, but not the concern of ChatClient. ChatClient only cares that it can orchestrate the sending and browsing of messages effectively. We can guarantee consistent behavior of ChatClient’s dependencies and of the client itself without access to these detailed decisions by testing with TestApi.
TestApi != system interface: Having the test suite define it’s own api allows our test to define expectations differently than the system. In this case, it allows us to create users even if the system interface doesn’t have a mechanism to do so.
Abstract test class + SutFactory: The test suite is defined as abstract and requires any derivatives to implement the SutFactory. This allows us to control all lifetimes of test api instances within the test suite while allowing derivative test suites to vary how the test subject is constructed.
This may seem like a lot of added code compared to directly testing a component. However, we get to share the core test suite across all implementations, resulting in overall less code!
Example: Testing a service with multiple configurations#
The same approach we used to test an abstract dependency applies for testing the main service. We have an abstract test class with thin derivatives that implement the test api.
The main difference is that the service usually won’t have separate implementations. Instead, we want to test the service with different dependency configurations.
This is even easier. We only need to write one test api implementation that accepts an injected instance of the service. We can then derive test suites for any dependency configuration simply.
publicclassChatClientTestApi: ChatClientTests.ITestApi{
// A test API ChatClient chatClient;
public ChatClientTestApi(ChatClient chatClient){
this.chatClient = chatClient;
}
// implemented api methods go here}
publicclassUnitChatClientTests : ChatClientTests{
publicoverride ChatClientTests.ITestApi SutFactory(){
// TODO: create an instance of ChatClient with test double dependency implementations ChatClient client = new ChatClient(new InMemoryThreadAccess(), new InMemoryUserAccess(), new InMemoryAttachmentAccess(), new SpyChatNotifier());
return ChatClientTestApi(client);
}
}
publicclassIntegrationChatClientTests : ChatClientTests{
publicoverride ChatClientTests.ITestApi SutFactory(){
// TODO: create an instance of ChatClient with fully-fledged dependencies ChatClient client = new ChatClient(new SqlThreadAccess(), new ActiveDirectoryUserAccess(), new FileSystemAttachmentAccess(), new EmailChatNotifier());
return ChatClientTestApi(client);
}
}
C# users of TestApi can still benefit from test reuse. The use of inheritance makes the reuse less straightforward compared to F#, but the overhead is still fairly low and the pattern is fairly easy to trace. I’ve certainly found it worth the flexibility to test different configurations or implementations without duplicating tests.