Integration Testing with Akka.Hosting.TestKit
8 minutes to readWe introduced the Akka.TestKit earlier in Unit 1 of Akka.NET Bootcamp - and one of the problems we mentioned with it is that it still relies on HOCON, Akka.NET’s legacy / internal configuration system.
In this lesson we are going to introduce the Akka.Hosting.TestKit, a fusion of Akka.Hosting
and the Akka.TestKit that is aimed at making it easier to write integration tests that touch Akka.NET actors and, optionally, the dependencies injected into them.
Starts at the appropriate timestamp for this lesson
Akka.Hosting.TestKit
Let’s install the Akka.Hosting.TestKit into our AkkaWordCounter2.App.Tests
project, if it’s not installed already.
dotnet add package Akka.Hosting.TestKit
A small bummer for NUnit and MSTest users: the Akka.Hosting.TestKit is currently xUnit-only.
The Akka.Hosting.TestKit has the following goals:
- Create an
IHost
for each test method; - Create a real
IServiceCollection
andIServiceProvider
for each test method - NO FAKES OR MOCKS, we’re going to use the realMicrosoft.Extensions.DependencyInjection
system to do the real thing on our code-under-test; - Bring all of the same testing functionality of the Akka.TestKit into an environment where we’re using
AkkaConfigurationBuilder
to configure our actors rather than arranging it as part of the test methods themselves.
The Akka.Hosting.TestKit is so convenient for testing Microsoft.Extensions-based code that we often use it for testing non-Akka.NET cases too.
Writing Our First Integration Test
Inside AkkaWordCounter2.App.Tests
please create a new file called ParserActorSpecs.cs
- we will test the ParserActor
to verify its functionality.
Inside ParserActorSpecs.cs
please type the following:
using Akka.Hosting;
using AkkaWordCounter2.App.Actors;
using AkkaWordCounter2.App.Config;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Xunit.Abstractions;
namespace AkkaWordCounter2.App.Tests;
public class ParserActorSpecs : Akka.Hosting.TestKit.TestKit
{
public ParserActorSpecs(ITestOutputHelper output) : base(output: output)
{
}
protected override void ConfigureServices(HostBuilderContext context, IServiceCollection services)
{
services.AddHttpClient();
}
protected override void ConfigureAkka(AkkaConfigurationBuilder builder, IServiceProvider provider)
{
builder
.ConfigureLoggers(configBuilder =>
{
configBuilder.LogLevel = Akka.Event.LogLevel.DebugLevel;
})
.AddParserActors();
}
public static readonly AbsoluteUri ParserActorUri = new(new Uri("https://getakka.net/"));
[Fact]
public async Task ShouldParseWords()
{
// arrange
var parserActor = await ActorRegistry.GetAsync<ParserActor>();
var expectResultsProbe = CreateTestProbe();
// act
parserActor.Tell(new DocumentCommands.ScanDocument(ParserActorUri), expectResultsProbe);
// assert
await expectResultsProbe.ExpectMsgAsync<DocumentEvents.WordsFound>(); // should get at least 1 WordsFound
await expectResultsProbe.FishForMessageAsync(m => m is DocumentEvents.EndOfDocumentReached); // should get EndOfDocumentReached
}
}
Now go ahead and execute this unit test in your IDE or using the dotnet test
command.
{DateTime}:INF:Microsoft.Hosting.Lifetime:0 Application started. Press Ctrl+C to shut down.
{DateTime}:INF:Microsoft.Hosting.Lifetime:0 Hosting environment: Production
{DateTime}:INF:Microsoft.Hosting.Lifetime:0 Content root path: C:\Repositories\Petabridge\akka-bootcamp\unit-1\completed\AkkaWordCounter2.App.Tests\bin\Debug\net9.0\
{DateTime}:INF:System.Net.Http.HttpClient.Default.LogicalHandler:RequestPipelineStart Start processing HTTP request GET https://getakka.net/
{DateTime}:INF:System.Net.Http.HttpClient.Default.ClientHandler:RequestStart Sending HTTP request GET https://getakka.net/
{DateTime}:INF:System.Net.Http.HttpClient.Default.ClientHandler:RequestEnd Received HTTP response headers after 467.1066ms - 200
{DateTime}:INF:System.Net.Http.HttpClient.Default.LogicalHandler:RequestPipelineEnd End processing HTTP request after 487.7466ms - 200
{DateTime}:INF:Microsoft.Hosting.Lifetime:0 Application is shutting down...
Our test is passing, excellent.
Configuring Akka.Hosting.TestKit Tests
Let’s break down what we just did with ParserActorSpecs
:
- Just like with the regular Akka.TestKit, our test fixture classes must inherit from Akka.Hosting.TestKit;
- We passed in xUnit’s
ITestOutputHelper
just like we did before, to ensure that our test output all gets captured appropriately by xUnit; - We overrode the
ConfigureServices
method body - this is optional, but this is where we register non-Akka.NET services we want to leverage in our test; and - Finally, we have to provide a method body for the
abstract
ConfigureAkka
method - this is where we used theAkkaConfigurationBuilder
extension methods we defined in “Akka.Hosting, Routers, and Dependency Injection.”
Inside the ConfigureAkka
method we call our AddParsers
configuration method we defined earlier:
protected override void ConfigureAkka(AkkaConfigurationBuilder builder, IServiceProvider provider)
{
builder
.ConfigureLoggers(configBuilder =>
{
configBuilder.LogLevel = Akka.Event.LogLevel.DebugLevel;
})
.AddParserActors();
}
This is how we get our production configuration under test coverage.
Accessing Our Actors
Now the one other big difference between the regular TestKit and Akka.Hosting.TestKit is how we start our actors - in the former we just start the actors up at the start of each test typically.
But in the Akka.Hosting.TestKit the actors are launched in the background via the implicit IHost
that is created for us, so the creation of our actors under test might happen before our test even begins to execute1.
Therefore, we usually end up accessing our actors via the ActorRegistry
- just like we might do inside our applications:
// arrange
var parserActor = await ActorRegistry.GetAsync<ParserActor>();
var expectResultsProbe = CreateTestProbe();
TestProbe
and FishForMessageAsync
Now there are two more things this test is doing that we haven’t seen before:
- Creating a new
TestProbe
- this is the equivalent to creating a secondTestActor
.TestProbe
s have all of the same functionality as theTestActor
, but it’s a manual instance of it that you can control. FishForMessageAsync
- we’re usingExpectAsync
in this test too, but you’ll notice a second call toFishForMessageAsync<T>
. What this method does is it filters through all of the receive messages theTestProbe
receives until it reaches the target message typeT
. Or, if 3 seconds passes beforeT
is found this method will throw an assertion exception instead.
Wrapping Up
We are almost done with Unit 1 of Akka.NET Bootcamp. The next thing we need to do is configure AkkaWordCounter2
using Microsoft.Extensions.Configuration and the IOptions
pattern.
Further Reading
- How to Test Akka.NET Applications Pt 1: How to Make Actors Easily Testable
- How to Test Akka.NET Applications Pt 2: Writing Akka.NET Unit & Integration Tests with Akka.TestKit
-
You can still, if you want to, create actors using
Sys.ActorOf
inside an Akka.Hosting.TestKit test - nothing is stopping you from doing that. But since the idea is to test our real configurations our application uses, usually you’re going to end up using theActorRegistry
to access actors that were declared as part of theConfigureAkka
method ↩