Testing
The Dale TestKit provides a minimal actor context for unit testing logic blocks. It records messages, mocks dependencies, and exposes helpers for verifying outputs, property changes, timers, and I/O behavior.
Overview
- TestKit provides a lightweight actor context that captures all outgoing messages.
- Built on Moq for mocking dependencies.
- Tests follow the Arrange-Act-Assert pattern.
- No real runtime or hardware is needed; everything is simulated in-process.
Quick Start
For simple tests where you only need an initialized block:
var block = new MyBlock(LogicBlockTestHelper.CreateLoggerMock().Object);
block.InitializeForTest();
// block is now ready to testInitializeForTest() runs the block through its initialization lifecycle so properties and I/O are wired up and ready.
Test Context Builder
For full control over the test environment, use the test context builder. This lets you configure interface mappings, persistent values, and injected services.
var testContext = block.CreateTestContext()
.WithLogicInterfaceMapping<IMyInterface>(remoteBlockId)
.WithPersistentValue(b => b.SavedValue, 42)
.WithServices(services => services.AddSingleton(mockService.Object))
.Build();| Method | Purpose |
|---|---|
WithLogicInterfaceMapping | Wire a service interface to a remote block ID. |
WithPersistentValue | Pre-load a persisted property value. |
WithServices | Register additional services into the DI container. |
Build | Finalize and return the test context. |
Verifying Messages
After triggering an action on the block, verify that it sent the expected messages:
testContext.VerifySendRequest<MyRequest>(toId, msg => Assert.Equal(10, msg.Value));
testContext.VerifySendStateUpdate<MyUpdate>();
testContext.VerifySendCommand<MyCommand>(toId, times: Times.Once());VerifySendRequestchecks that a request message was sent to a specific target with matching content.VerifySendStateUpdatechecks that a state update was broadcast.VerifySendCommandchecks that a command was sent, with optional call-count verification.
Verifying Property Changes
Verify that service properties and measuring points changed to expected values:
testContext.VerifyServicePropertyChanged(b => b.Temperature, val => Assert.Equal(22.0, val));
testContext.VerifyServiceMeasuringPointChanged(b => b.Power, val => Assert.True(val > 0));The lambda selector identifies the property by expression, and the assertion callback validates the new value.
Testing Timers
Fire timer callbacks manually and inspect their configured intervals:
block.FireTimer(lb => lb.OnTimer());
var interval = block.GetTimerInterval(lb => lb.OnTimer());
Assert.Equal(TimeSpan.FromSeconds(5), interval);FireTimerinvokes the timer handler immediately without waiting for the real interval.GetTimerIntervalreturns theTimeSpanthe block registered for that timer.
Testing I/O
Simulate hardware input changes and verify output writes:
// Digital I/O
block.DigitalInput.RaiseInputChanged(true);
testContext.VerifyDigitalOutputSet(block.Led, true);
// Analog I/O
block.TemperatureSensor.RaiseInputChanged(22.5);
testContext.VerifyAnalogOutputSet(block.Heater, value: 0.8, tolerance: 0.1);RaiseInputChangedsimulates a hardware event on an input.VerifyDigitalOutputSetasserts that a digital output was set to the expected boolean value.VerifyAnalogOutputSetasserts that an analog output was set within a tolerance range.
Flushing Pending Actions
When your block uses InvokeSynchronizedAfter to schedule deferred work, flush those pending actions before asserting:
testContext.FlushPendingActions();This processes all queued callbacks synchronously so you can verify their side effects immediately.
Complete Example
A full test class for the PingPong sample, testing initialization, message handling, pause behavior, and timer interaction:
public class PingBlockTests
{
private readonly PingBlock _block;
private readonly LogicBlockTestContext _testContext;
private readonly LogicBlockId _pongBlockId = new("pong-block-1");
public PingBlockTests()
{
_block = new PingBlock(LogicBlockTestHelper.CreateLoggerMock().Object);
_testContext = _block.CreateTestContext()
.WithLogicInterfaceMapping<IPong>(_pongBlockId)
.WithPersistentValue(b => b.TotalPings, 0)
.Build();
}
[Fact]
public void InitialState_PingsPerSecondIsZero()
{
Assert.Equal(0, _block.PingsPerSecond);
Assert.False(_block.Pause);
}
[Fact]
public void SendsPingOnTimer()
{
_block.FireTimer(lb => lb.OnPingTimer());
_testContext.VerifySendRequest<PingRequest>(
_pongBlockId,
msg => Assert.True(msg.Timestamp > 0));
}
[Fact]
public void PauseStopsSendingPings()
{
_block.Pause = true;
_block.FireTimer(lb => lb.OnPingTimer());
_testContext.VerifySendRequest<PingRequest>(
_pongBlockId, times: Times.Never());
}
[Fact]
public void ReceivingPongUpdatesMeasuringPoint()
{
_block.HandleResponse(new PongResponse { PingId = 1 });
_testContext.VerifyServiceMeasuringPointChanged(
b => b.PingsPerSecond,
val => Assert.True(val > 0));
}
[Fact]
public void TimerIntervalIsFiveSeconds()
{
var interval = _block.GetTimerInterval(lb => lb.OnPingTimer());
Assert.Equal(TimeSpan.FromSeconds(5), interval);
}
[Fact]
public void PersistentValueIsRestored()
{
Assert.Equal(0, _block.TotalPings);
}
}Running Tests
Run all tests in your Dale project from the command line:
dale testThis discovers and executes all test projects in the solution, reporting results to the console.