Notes and documentation covering actors, remoting, clustering, persistence, streams, serialization, hosting, testing, and best practices for the Akka.NET framework used throughout the ScadaLink system.
193 lines
6.3 KiB
Markdown
193 lines
6.3 KiB
Markdown
# 17 — Akka.Hosting.TestKit
|
|
|
|
## Overview
|
|
|
|
Akka.Hosting.TestKit extends the standard TestKit with full `Microsoft.Extensions.Hosting` integration. It spins up a complete host environment with DI, logging, configuration, and Akka.NET — making it ideal for integration tests that need the full application wiring.
|
|
|
|
In the SCADA system, Hosting.TestKit is used to test the full actor startup pipeline: DI-injected actors, protocol adapter factories, configuration loading, and the Device Manager singleton lifecycle — all within a controlled test environment.
|
|
|
|
## When to Use
|
|
|
|
- Integration tests that require DI (protocol adapter factories, configuration services, loggers)
|
|
- Testing the full Akka.Hosting configuration pipeline (Remoting, Cluster, Singleton registration)
|
|
- Verifying that the ActorRegistry correctly resolves actor references
|
|
- Testing actors that depend on `IServiceProvider` or `IOptions<T>`
|
|
|
|
## When Not to Use
|
|
|
|
- Simple actor unit tests that don't need DI — use Akka.TestKit directly (see [16-TestKit.md](./16-TestKit.md))
|
|
- Multi-node cluster tests — use MultiNodeTestRunner (see [18-MultiNodeTestRunner.md](./18-MultiNodeTestRunner.md))
|
|
- Performance tests — the hosting overhead adds latency that distorts benchmarks
|
|
|
|
## Design Decisions for the SCADA System
|
|
|
|
### Test Fixture Structure
|
|
|
|
Each integration test class inherits from `Akka.Hosting.TestKit.TestKit` and overrides configuration methods:
|
|
|
|
```csharp
|
|
public class DeviceManagerIntegrationTests : Akka.Hosting.TestKit.TestKit
|
|
{
|
|
public DeviceManagerIntegrationTests(ITestOutputHelper output)
|
|
: base(output: output) { }
|
|
|
|
protected override void ConfigureServices(HostBuilderContext context, IServiceCollection services)
|
|
{
|
|
// Register test doubles for protocol adapters
|
|
services.AddSingleton<IProtocolAdapterFactory, MockProtocolAdapterFactory>();
|
|
services.AddSingleton<IOptions<SiteConfiguration>>(
|
|
Options.Create(TestSiteConfig.TwoDevices()));
|
|
}
|
|
|
|
protected override void ConfigureAkka(AkkaConfigurationBuilder builder, IServiceProvider provider)
|
|
{
|
|
builder
|
|
.WithActors((system, registry, resolver) =>
|
|
{
|
|
var manager = system.ActorOf(
|
|
resolver.Props<DeviceManagerActor>(), "device-manager");
|
|
registry.Register<DeviceManagerActor>(manager);
|
|
});
|
|
}
|
|
|
|
[Fact]
|
|
public async Task DeviceManager_should_create_device_actors_from_config()
|
|
{
|
|
var manager = await ActorRegistry.GetAsync<DeviceManagerActor>();
|
|
manager.Tell(new GetManagedDevices());
|
|
|
|
var devices = ExpectMsg<ManagedDeviceList>();
|
|
Assert.Equal(2, devices.Devices.Count);
|
|
}
|
|
}
|
|
```
|
|
|
|
### Mock Protocol Adapters
|
|
|
|
Register mock protocol adapters that simulate equipment behavior without requiring actual network connections:
|
|
|
|
```csharp
|
|
public class MockProtocolAdapterFactory : IProtocolAdapterFactory
|
|
{
|
|
public Props CreateAdapterProps(DeviceConfig config, IDependencyResolver resolver)
|
|
{
|
|
return Props.Create(() => new MockDeviceActor(config));
|
|
}
|
|
}
|
|
```
|
|
|
|
### Testing Configuration Loading
|
|
|
|
Verify that site-specific configuration is correctly loaded and applied:
|
|
|
|
```csharp
|
|
protected override void ConfigureServices(HostBuilderContext context, IServiceCollection services)
|
|
{
|
|
// Load test configuration
|
|
var config = new ConfigurationBuilder()
|
|
.AddJsonFile("test-appsettings.json")
|
|
.Build();
|
|
|
|
services.Configure<SiteConfiguration>(config.GetSection("ScadaSite"));
|
|
}
|
|
```
|
|
|
|
## Common Patterns
|
|
|
|
### ActorRegistry Assertions
|
|
|
|
Use `ActorRegistry.GetAsync<T>()` to verify actors were correctly registered during startup:
|
|
|
|
```csharp
|
|
[Fact]
|
|
public async Task Should_register_all_required_actors()
|
|
{
|
|
var manager = await ActorRegistry.GetAsync<DeviceManagerActor>();
|
|
Assert.NotNull(manager);
|
|
|
|
var health = await ActorRegistry.GetAsync<ClusterHealthActor>();
|
|
Assert.NotNull(health);
|
|
}
|
|
```
|
|
|
|
### Testing with TestProbe
|
|
|
|
TestProbe works alongside Hosting.TestKit:
|
|
|
|
```csharp
|
|
[Fact]
|
|
public async Task DeviceManager_should_forward_commands()
|
|
{
|
|
var probe = CreateTestProbe();
|
|
var manager = await ActorRegistry.GetAsync<DeviceManagerActor>();
|
|
|
|
// Tell the manager to use the probe for a specific device
|
|
manager.Tell(new RegisterTestDevice("test-device", probe));
|
|
manager.Tell(new SendCommand("cmd-1", "test-device", "Start", true));
|
|
|
|
probe.ExpectMsg<SendCommand>(cmd => cmd.CommandId == "cmd-1");
|
|
}
|
|
```
|
|
|
|
### Lifecycle Testing
|
|
|
|
Verify that the actor system starts and stops cleanly:
|
|
|
|
```csharp
|
|
[Fact]
|
|
public async Task Should_shutdown_gracefully()
|
|
{
|
|
var manager = await ActorRegistry.GetAsync<DeviceManagerActor>();
|
|
Watch(manager);
|
|
|
|
// Trigger coordinated shutdown
|
|
await CoordinatedShutdown.Get(Sys).Run(CoordinatedShutdown.ClrExitReason.Instance);
|
|
|
|
ExpectTerminated(manager);
|
|
}
|
|
```
|
|
|
|
## Anti-Patterns
|
|
|
|
### Duplicating Production Configuration
|
|
|
|
Test configuration should be minimal and focused on the test scenario. Do not copy the entire production Akka.Hosting setup into tests — only configure what the test needs. This prevents tests from breaking when production config changes.
|
|
|
|
### Testing Cluster Behavior in Hosting.TestKit
|
|
|
|
Hosting.TestKit runs a single-node ActorSystem. Do not test cluster membership, failover, or Singleton migration here — those require MultiNodeTestRunner. Hosting.TestKit is for testing application-level actor behavior with DI, not distributed system behavior.
|
|
|
|
### Slow Tests from Full Hosting Stack
|
|
|
|
Each Hosting.TestKit test spins up a full host. If you have hundreds of tests, this adds up. Reserve Hosting.TestKit for integration tests that genuinely need DI; use plain TestKit for unit tests.
|
|
|
|
## Configuration Guidance
|
|
|
|
### NuGet Package
|
|
|
|
```
|
|
Akka.Hosting.TestKit
|
|
```
|
|
|
|
This pulls in `Akka.Hosting`, `Akka.TestKit.Xunit2`, and `Microsoft.Extensions.Hosting.Testing`.
|
|
|
|
### Test Project Structure
|
|
|
|
```
|
|
ScadaSystem.Tests/
|
|
Unit/
|
|
DeviceActorTests.cs (plain TestKit)
|
|
CommandHandlerTests.cs (plain TestKit)
|
|
Integration/
|
|
DeviceManagerIntegTests.cs (Hosting.TestKit)
|
|
ConfigurationIntegTests.cs (Hosting.TestKit)
|
|
Fixtures/
|
|
TestSiteConfig.cs
|
|
MockProtocolAdapterFactory.cs
|
|
```
|
|
|
|
## References
|
|
|
|
- NuGet: <https://www.nuget.org/packages/Akka.Hosting.TestKit>
|
|
- Petabridge Bootcamp: <https://petabridge.com/bootcamp/lessons/unit-1/akka-hosting-testkit/>
|