# 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<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:

```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<SendCommand>();
    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<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:

```csharp
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):

```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<Connected>();
    deviceActor.Tell(new SubscribeToTag("temperature"));
    await ExpectMsgAsync<TagSubscribed>();
});
```

### Mock Protocol Adapters

Create test doubles for protocol adapters that simulate equipment behavior:

```csharp
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:

```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: <https://getakka.net/articles/actors/testing-actor-systems.html>
- Akka.TestKit.Xunit2 NuGet: <https://www.nuget.org/packages/Akka.TestKit.Xunit2>
