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.
7.2 KiB
16 — TestKit (Akka.TestKit)
Overview
Akka.TestKit provides infrastructure for unit and integration testing of actors. It creates a controlled test environment with a test ActorSystem, TestProbe actors for asserting message flows, TestActorRef for synchronous actor access, and EventFilter for asserting log output. Test-framework-specific adapters are available for xUnit, NUnit, and MSTest.
In the SCADA system, TestKit is essential for validating device actor behavior, protocol abstraction correctness, command handling logic, and supervision strategies — all without connecting to real equipment.
When to Use
- Unit testing individual actor behavior (device actors, command handlers, alarm processors)
- Testing message flows between actors (Device Manager → Device Actor → Protocol Adapter)
- Verifying supervision strategies (what happens when a device actor throws a
CommunicationException) - Testing actor state transitions (
Connecting→Online→Faulted) - Asserting that specific messages are sent in response to inputs
When Not to Use
- Testing non-actor code (protocol parsing logic, tag value conversions) — use standard unit tests
- Full integration tests with DI, hosting, and configuration — use Akka.Hosting.TestKit (see 17-HostingTestKit.md)
- Multi-node failover tests — use MultiNodeTestRunner (see 18-MultiNodeTestRunner.md)
Design Decisions for the SCADA System
Test Framework: xUnit
Use Akka.TestKit.Xunit2 to integrate with xUnit, which is the standard test framework for modern .NET projects:
NuGet: Akka.TestKit.Xunit2
Testing the Protocol Abstraction
The protocol abstraction (common message contract with OPC-UA and custom protocol implementations) is the most critical code to test. Create tests that verify both implementations respond identically to the same message contract:
public class OpcUaDeviceActorTests : TestKit
{
[Fact]
public void Should_respond_with_DeviceState_when_asked()
{
var deviceActor = Sys.ActorOf(Props.Create(() =>
new OpcUaDeviceActor(TestDeviceConfig.Create())));
deviceActor.Tell(new GetDeviceState());
var state = ExpectMsg<DeviceState>();
Assert.Equal(ConnectionStatus.Connecting, state.Status);
}
}
public class CustomProtocolDeviceActorTests : TestKit
{
[Fact]
public void Should_respond_with_DeviceState_when_asked()
{
var deviceActor = Sys.ActorOf(Props.Create(() =>
new CustomProtocolDeviceActor(TestDeviceConfig.Create())));
deviceActor.Tell(new GetDeviceState());
var state = ExpectMsg<DeviceState>();
Assert.Equal(ConnectionStatus.Connecting, state.Status);
}
}
TestProbe for Interaction Testing
Use TestProbe to verify that actors send the correct messages to their collaborators:
[Fact]
public void DeviceManager_should_forward_command_to_correct_device()
{
var probe = CreateTestProbe();
// Create a DeviceManager that uses the probe as a device actor
var manager = Sys.ActorOf(Props.Create(() =>
new TestableDeviceManager(deviceActorOverride: probe)));
manager.Tell(new SendCommand("cmd-1", "machine-001", "StartMotor", true));
// Verify the command was forwarded to the device actor
var forwarded = probe.ExpectMsg<SendCommand>();
Assert.Equal("cmd-1", forwarded.CommandId);
}
Testing Supervision Strategies
Verify that the Device Manager restarts device actors on communication failures:
[Fact]
public void Should_restart_device_actor_on_communication_exception()
{
var probe = CreateTestProbe();
var deviceActor = Sys.ActorOf(Props.Create(() =>
new FailingDeviceActor(failOnFirst: true)));
Watch(deviceActor);
// Send a message that triggers the CommunicationException
deviceActor.Tell(new ConnectToDevice());
// The actor should be restarted, not stopped
ExpectNoMsg(TimeSpan.FromSeconds(1)); // No Terminated message
// After restart, the actor should accept messages again
deviceActor.Tell(new GetDeviceState());
ExpectMsg<DeviceState>();
}
Common Patterns
ExpectMsg with Timeout
Always specify reasonable timeouts for message expectations. In CI environments, use the Dilated method to account for slower machines:
ExpectMsg<CommandAcknowledged>(TimeSpan.FromSeconds(5));
// or
ExpectMsg<CommandAcknowledged>(Dilated(TimeSpan.FromSeconds(3)));
ExpectNoMsg for Negative Testing
Verify that an actor does NOT send a message in certain conditions (e.g., a filtered tag update should not be forwarded):
deviceActor.Tell(new TagUpdate("temp", 20.0)); // Within deadband
ExpectNoMsg(TimeSpan.FromMilliseconds(500)); // Should be filtered
WithinAsync for Timing Assertions
Verify that a sequence of messages arrives within a time window:
await WithinAsync(TimeSpan.FromSeconds(5), async () =>
{
deviceActor.Tell(new ConnectToDevice());
await ExpectMsgAsync<Connected>();
deviceActor.Tell(new SubscribeToTag("temperature"));
await ExpectMsgAsync<TagSubscribed>();
});
Mock Protocol Adapters
Create test doubles for protocol adapters that simulate equipment behavior:
public class MockOpcUaClient : IOpcUaClient
{
private readonly Dictionary<string, object> _tagValues = new();
public Task<object> ReadTagAsync(string tagName) =>
Task.FromResult(_tagValues.GetValueOrDefault(tagName, 0.0));
public void SimulateTagChange(string tagName, object value) =>
_tagValues[tagName] = value;
}
Anti-Patterns
Testing Internal Actor State Directly
Avoid using TestActorRef to inspect internal actor state. Test behavior through messages — send an input, assert the output. This keeps tests decoupled from implementation details.
Shared Test ActorSystem
Each test class gets its own ActorSystem (TestKit creates one per fixture). Do not share an ActorSystem across test classes — actor names and state leak between tests.
Ignoring Dead Letters in Tests
Dead letters in tests often indicate bugs (messages sent to stopped actors, wrong actor references). Subscribe to dead letters in tests and fail on unexpected ones:
Sys.EventStream.Subscribe(TestActor, typeof(DeadLetter));
Flaky Time-Dependent Tests
Avoid tests that depend on exact timing (Thread.Sleep(100)). Use ExpectMsg with generous timeouts and Dilated for CI compatibility.
Configuration Guidance
Test-Specific Configuration
Pass custom configuration to TestKit for specific test scenarios:
public class DeviceActorTests : TestKit
{
public DeviceActorTests()
: base(ConfigurationFactory.ParseString(@"
akka.loglevel = DEBUG
akka.actor.debug.receive = on
"))
{
}
}
CI/CD Time Factor
Set the time factor for CI environments where machines are slower:
akka.test {
timefactor = 3.0 # Multiply all timeouts by 3 in CI
}
References
- Official Documentation: https://getakka.net/articles/actors/testing-actor-systems.html
- Akka.TestKit.Xunit2 NuGet: https://www.nuget.org/packages/Akka.TestKit.Xunit2