# 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](./17-HostingTestKit.md)) - Multi-node failover tests — use MultiNodeTestRunner (see [18-MultiNodeTestRunner.md](./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: ```csharp 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(); 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(); Assert.Equal(ConnectionStatus.Connecting, state.Status); } } ``` ### TestProbe for Interaction Testing Use `TestProbe` to verify that actors send the correct messages to their collaborators: ```csharp [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(); Assert.Equal("cmd-1", forwarded.CommandId); } ``` ### Testing Supervision Strategies Verify that the Device Manager restarts device actors on communication failures: ```csharp [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(); } ``` ## Common Patterns ### ExpectMsg with Timeout Always specify reasonable timeouts for message expectations. In CI environments, use the `Dilated` method to account for slower machines: ```csharp ExpectMsg(TimeSpan.FromSeconds(5)); // or ExpectMsg(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): ```csharp 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: ```csharp await WithinAsync(TimeSpan.FromSeconds(5), async () => { deviceActor.Tell(new ConnectToDevice()); await ExpectMsgAsync(); deviceActor.Tell(new SubscribeToTag("temperature")); await ExpectMsgAsync(); }); ``` ### Mock Protocol Adapters Create test doubles for protocol adapters that simulate equipment behavior: ```csharp public class MockOpcUaClient : IOpcUaClient { private readonly Dictionary _tagValues = new(); public Task 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: ```csharp 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: ```csharp 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: ```hocon akka.test { timefactor = 3.0 # Multiply all timeouts by 3 in CI } ``` ## References - Official Documentation: - Akka.TestKit.Xunit2 NuGet: