I'm trying to write tests for an Automatonymous state machine, but I'm having a fair bit of trouble getting it right, and I've found very little documentation.
Here's what I have at the moment for one test:
[TestFixture]
public class MyProcessStateMachineTests
{
InMemoryTestHarness _Harness;
MyProcessStateMachine _Machine;
StateMachineSagaTestHarness<MyProcess, MyProcessStateMachine> _Saga;
[OneTimeSetUp]
public void ConfigureMessages()
{
MessageCorrelation.UseCorrelationId<RequestMyDetails>(x => x.CorrelationId);
MessageCorrelation.UseCorrelationId<FileAttached>(x => x.CorrelationId);
MessageCorrelation.UseCorrelationId<PDFGenerated>(x => x.CorrelationId);
MessageCorrelation.UseCorrelationId<CustomerAttachFile>(x => x.CorrelationId);
MessageCorrelation.UseCorrelationId<AddCustomerNote>(x => x.CorrelationId);
MessageCorrelation.UseCorrelationId<EmailPublished>(x => x.CorrelationId);
}
[SetUp]
public void InitializeTestHarness()
{
_Harness = new InMemoryTestHarness();
_Machine = new MyProcessStateMachine( /* snip */ );
_Saga = _Harness.StateMachineSaga<MyProcess, MyProcessStateMachine>(_Machine);
_Harness.Start().Wait();
}
[TearDown]
public void StopTestHarness()
{
_Harness.Stop();
}
[Test]
public async Task ShouldAttachToCustomer()
{
var sagaId = Guid.NewGuid();
var custId = Guid.NewGuid();
var fileAttached = BuildFileAttachedMessage(sagaId);
await _Harness.InputQueueSendEndpoint.Send(BuildStartMessage(sagaId));
await _Harness.InputQueueSendEndpoint.Send(BuildDetailsReceivedMessage(sagaId));
await _Harness.InputQueueSendEndpoint.Send(BuildPdfGeneratedMessage(sagaId));
await _Harness.InputQueueSendEndpoint.Send(fileAttached);
// Next line is based on [the answer here][1]
// Once the above messages are all consumed and processed,
// the state machine should be in AwaitingEmail state
await _Saga.Match(x =>
x.CorrelationId == sagaId
&& x.CurrentState == _Machine.AwaitingEmail.Name,
new TimeSpan(0, 0, 30));
// Grab the instance and Assert stuff...
}
// Snip...
}
Given that the _Saga.Match call finds a match, I would expect that all messages have been processed and I should be able to grab my state machine instance and published events and check their values - but that isn't the case. When I run the tests in the fixture, sometimes the instance I get has consumed and published the expected messages; sometimes it's not quite there yet.
I've tried grabbing my instance using:
var inst = _Saga.Sagas.FirstOrDefault(x => x.Saga.CorrelationId == sagaId);
or grabbing published events with:
var test = _Harness.Published
.FirstOrDefault(x => x.MessageType == typeof(IAttachFile) && x.Context.CorrelationId == sagaId);
but it doesn't matter that the call to Match succeeded, the state machine instance (and published events) aren't always present.
I'm assuming that the async proccesses from Automatonymous, MassTransit, or test harness is causing the inconsistency. Any help?
Testing with MassTransit, MassTransit.Automatonymous and MassTransit.TestFramework 5.1.2.1528, Automatonymous 4.1.1.102,
EDIT:
Further review, I've found that when I have a problem, the call to Match( ... )
didn't succeed - it timed out. (I had been incorrectly assuming that a timeout would throw an exception.)
In case this might be helpful to someone else, this is how I eventually got it working:
[TestFixture]
public class ProcessStateMachineTests : InMemoryTestFixture
{
TimeSpan _TestTimeout = new TimeSpan(0, 1, 0);
ProcessStateMachine _Machine;
InMemorySagaRepository<Process> _Repository;
protected override void ConfigureInMemoryReceiveEndpoint(
IInMemoryReceiveEndpointConfigurator configurator)
{
_Machine = new ProcessStateMachine();
_Repository = new InMemorySagaRepository<Process>();
configurator.StateMachineSaga(_Machine, _Repository);
}
[OneTimeSetUp]
public void ConfigureMessages()
{
// Message correlation and such happens in here
ProcessStateMachine.ConfigureMessages();
}
[Test]
public async Task OnInitializationIStartProcessIsConsumed()
{
var sagaId = Guid.NewGuid();
var customerId = Guid.NewGuid();
await SetupStateMachine(sagaId, customerId, _Machine.AwaitingDetails.Name);
var msg = InMemoryTestHarness.Consumed
.Select<IStartProcess>(x => x.Context.Message.RequestId == sagaId)
.FirstOrDefault();
// Assert against msg for expected results
}
[Test]
public async Task OnStartProcessAddCustomerNoteAndRequestDetailsPublished()
{
var sagaId = Guid.NewGuid();
var customerId = Guid.NewGuid();
await SetupStateMachine(sagaId, customerId, _Machine.AwaitingDetails.Name);
var pubdNoteAddedMsg = InMemoryTestHarness.Published
.Select<IAddCustomerNote>()
.FirstOrDefault(x => x.Context.Message.RequestId == sagaId);
var pubdDetailsReqdMsg = InMemoryTestHarness.Published
.Select<IRequestDetails>()
.FirstOrDefault(x => x.Context.Message.RequestId == sagaId);
Assert.IsTrue(pubdNoteAddedMsg != null);
Assert.IsTrue(pubdDetailsReqdMsg != null);
Assert.AreEqual(sagaId, pubdNoteAddedMsg.Context.CorrelationId);
Assert.AreEqual(sagaId, pubdDetailsReqdMsg.Context.CorrelationId);
Assert.AreEqual(customerId, pubdNoteAddedMsg.Context.Message.CustomerId);
Assert.IsFalse(String.IsNullOrEmpty(pubdNoteAddedMsg.Context.Message.Note));
}
private async Task SetupStateMachine(
Guid sagaId,
Guid customerId,
String toState)
{
if (String.IsNullOrEmpty(toState))
return;
await MoveStateMachineForward(BuildStartMessage(), x => x.AwaitingDetails);
var awaitingDetailsId = await _Repository.ShouldContainSagaInState(
sagaId, _Machine, x => x.AwaitingDetails, _TestTimeout);
Assert.IsNotNull(awaitingDetailsId, "Error, expected state machine in AwaitingDetails state");
if (toState == _Machine.AwaitingDetails.Name)
return;
// ...and more stuff to move to later states, depending on
// where I want my test's starting point to be...
async Task MoveStateMachineForward<T>(
T message,
Func<ProcessStateMachine, Automatonymous.State> targetState)
where T : class
{
await InputQueueSendEndpoint.Send(message);
var foundSagaId = await _Repository.ShouldContainSagaInState(
sagaId, _Machine, targetState, _TestTimeout);
Assert.IsTrue(foundSagaId.HasValue);
}
IStartProcess BuildStartMessage()
{
return new StartProcessMessage(sagaId, customerId);
}
}
}