Testing with DxMessaging¶
<- Back to Index | Patterns | Runtime Configuration
You're here because: You want to write reliable, isolated tests for systems that use DxMessaging.
Key Testing Principles¶
- Use isolated MessageBus instances - Avoid global state pollution between tests
- Manage token lifecycles explicitly - Enable/disable tokens to control when handlers are active
- Clean up spawned objects - Destroy test GameObjects to prevent registration leaks
1) Isolated MessageBus for Unit Tests¶
The global MessageHandler.MessageBus is shared across your application. For unit tests, create a dedicated MessageBus instance to ensure complete isolation.
C#
using DxMessaging.Core;
using DxMessaging.Core.MessageBus;
using NUnit.Framework;
using UnityEngine;
[Test]
public void IsolatedBusDoesNotAffectGlobalBus()
{
// Create isolated bus and handler
MessageBus testBus = new();
GameObject testObject = new("TestObject");
MessageHandler handler = new(testObject) { active = true };
MessageRegistrationToken token = MessageRegistrationToken.Create(handler, testBus);
token.Enable();
int localCount = 0;
_ = token.RegisterUntargeted<MyMessage>(_ => localCount++);
// Emit to isolated bus only
MyMessage msg = new();
msg.EmitUntargeted(testBus);
Assert.AreEqual(1, localCount, "Local handler should receive message.");
// Cleanup
token.UnregisterAll();
Object.DestroyImmediate(testObject);
}
2) Test Base Class Pattern¶
Create a reusable base class for common setup/teardown.
C#
public abstract class MessagingTestBase
{
protected readonly List<GameObject> _spawned = new();
[TearDown]
public virtual void Cleanup()
{
foreach (GameObject spawned in _spawned)
if (spawned != null) Object.Destroy(spawned);
_spawned.Clear();
}
[UnitySetUp]
public virtual IEnumerator UnitySetup()
{
IMessageBus bus = MessageHandler.MessageBus;
while (bus.RegisteredUntargeted != 0 || bus.RegisteredTargeted != 0)
yield return null;
}
protected MessageRegistrationToken GetToken(MessageAwareComponent c) => c.Token;
}
3) Verifying Message Emission and Handling¶
Use counters or captured values to verify messages are emitted and handled correctly.
C#
[UnityTest]
public IEnumerator TargetedMessageReachesCorrectTarget()
{
GameObject target = new("Target", typeof(EmptyMessageAwareComponent));
GameObject nonTarget = new("NonTarget", typeof(EmptyMessageAwareComponent));
_spawned.Add(target);
_spawned.Add(nonTarget);
MessageRegistrationToken targetToken = GetToken(target.GetComponent<EmptyMessageAwareComponent>());
MessageRegistrationToken nonTargetToken = GetToken(nonTarget.GetComponent<EmptyMessageAwareComponent>());
int targetReceived = 0;
int nonTargetReceived = 0;
_ = targetToken.RegisterGameObjectTargeted<DamageMessage>(target, _ => targetReceived++);
_ = nonTargetToken.RegisterGameObjectTargeted<DamageMessage>(nonTarget, _ => nonTargetReceived++);
DamageMessage msg = new() { amount = 10 };
msg.EmitGameObjectTargeted(target);
Assert.AreEqual(1, targetReceived, "Target should receive message.");
Assert.AreEqual(0, nonTargetReceived, "Non-target should NOT receive message.");
yield break;
}
4) Testing Interceptors¶
Interceptors can cancel or modify messages. Test both scenarios.
C#
[UnityTest]
public IEnumerator InterceptorCanCancelMessage()
{
GameObject host = new("Host", typeof(EmptyMessageAwareComponent));
_spawned.Add(host);
MessageRegistrationToken token = GetToken(host.GetComponent<EmptyMessageAwareComponent>());
int handlerCalled = 0;
_ = token.RegisterUntargeted<TestMessage>(_ => handlerCalled++);
_ = token.RegisterUntargetedInterceptor((ref TestMessage _) => false); // Cancel
TestMessage msg = new();
msg.EmitUntargeted();
Assert.AreEqual(0, handlerCalled, "Handler must not run when interceptor cancels.");
yield break;
}
[UnityTest]
public IEnumerator InterceptorCanModifyMessage()
{
GameObject host = new("Host", typeof(EmptyMessageAwareComponent));
_spawned.Add(host);
MessageRegistrationToken token = GetToken(host.GetComponent<EmptyMessageAwareComponent>());
int receivedAmount = 0;
_ = token.RegisterUntargeted<DamageMessage>(msg => receivedAmount = msg.amount);
_ = token.RegisterUntargetedInterceptor((ref DamageMessage msg) =>
{
if (msg.amount > 100) msg = new DamageMessage { amount = 100 };
return true;
});
DamageMessage msg = new() { amount = 999 };
msg.EmitUntargeted();
Assert.AreEqual(100, receivedAmount, "Interceptor should clamp damage to 100.");
yield break;
}
5) Testing Component Lifecycle¶
Verify handlers respect Unity's enable/disable lifecycle.
C#
[UnityTest]
public IEnumerator DisabledComponentDoesNotReceiveMessages()
{
GameObject host = new("Host", typeof(EmptyMessageAwareComponent));
_spawned.Add(host);
EmptyMessageAwareComponent component = host.GetComponent<EmptyMessageAwareComponent>();
MessageRegistrationToken token = GetToken(component);
int count = 0;
_ = token.RegisterUntargeted<TestMessage>(_ => count++);
TestMessage msg1 = new();
msg1.EmitUntargeted();
Assert.AreEqual(1, count, "Enabled component should receive message.");
component.enabled = false;
yield return null;
TestMessage msg2 = new();
msg2.EmitUntargeted();
Assert.AreEqual(1, count, "Disabled component should NOT receive message.");
component.enabled = true;
yield return null;
TestMessage msg3 = new();
msg3.EmitUntargeted();
Assert.AreEqual(2, count, "Re-enabled component should receive message.");
}
6) Integration Testing¶
For integration tests, use the global bus with proper cleanup. Create GameObjects with components, emit messages, and verify system-wide behavior across multiple components.
Best Practices¶
| Practice | Why |
|---|---|
Use isolated MessageBus for unit tests | Prevents test pollution and flakiness |
| Track spawned objects in a list | Ensures cleanup even if test fails |
Use yield return null after state changes | Allows Unity lifecycle methods to execute |
| Test both positive and negative cases | Verify messages reach correct targets AND miss incorrect ones |
| Use expressive assertion messages | Makes test failures easier to diagnose |
Quick Reference: Test Message Setup¶
C#
// Test messages
[DxUntargetedMessage]
public partial struct TestMessage { }
[DxTargetedMessage]
[DxAutoConstructor]
public readonly partial struct DamageMessage { public readonly int amount; }
// Minimal test component - registers no handlers by default
public sealed class EmptyMessageAwareComponent : MessageAwareComponent { }
See Also¶
- Patterns - Pattern #6 (Local Bus Islands) and #12 (Testing)
- Runtime Configuration - MessageBus configuration options
- Back to Documentation Hub - Browse all docs