1

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.)

Remi Despres-Smyth
  • 4,173
  • 3
  • 36
  • 46
  • Check these tests, they actually show how test harness can be used to test state machine saga https://github.com/MassTransit/MassTransit/blob/develop/src/MassTransit.AutomatonymousIntegration.Tests/Testing_Specs.cs – Alexey Zimarev Jun 22 '18 at 17:04
  • Yes, I had checked those tests before. Tried again, in case I missed something the first time around. Used checks based on _Harness.Consumed, fetched instance form _Saga.Created (as in example test Using_the_testing_framework_built_into_masstransit). Ran tests with and without call to _Saga.Match, debugging with breakpoints in some tests, not in others. Asserting against the instance's current CurrentState sometimes passes, usually fails. Seems to work more often on tests where I'm only consuming one or two messages. If I try to move further into the state machine, things don't. – Remi Despres-Smyth Jun 25 '18 at 14:27
  • I would avoid using `MassTransit.TestFramework` also because it is coupled to NUnit. We are testing everything with `MassTransit.Testing` and have no issues. – Alexey Zimarev Jun 25 '18 at 17:46
  • @AlexeyZimarev is MassTransit.Testing still around? – balintn Oct 15 '19 at 08:43
  • For anyone hitting the same issue: the way you try to get the instance var inst = _Saga.Sagas.FirstOrDefault(x => x.Saga.CorrelationId == sagaId); – balintn Oct 15 '19 at 11:02
  • In my case, the issue was, in fact, problems with the state machine itself, which I didn't realize. It wasn't clear to me at the time how to determine the issue with the state machine. Seems obvious in hindsight. – Remi Despres-Smyth Oct 28 '19 at 17:27

2 Answers2

0

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);
        }
    }
}
Remi Despres-Smyth
  • 4,173
  • 3
  • 36
  • 46
0

For anyone hitting the same issue: the way you try to get the instance

var inst = _Saga.Sagas.FirstOrDefault(x => x.Saga.CorrelationId == sagaId);

can be fixed like this

var instanceIds = await _sagaHarness.Match(
    instance => instance.CorrelationId == sagaId
        && instance.CurrentState == _machine.Working.Name,
        new TimeSpan(0, 0, 30));

It is important that the expected state is included in the condition. Just fetching the instance by correlationid and then testing CurrentState may fail, as setting state (saga execution being async) may take some time. See full example at https://github.com/balintn22/AutomatonymousTestExample

balintn
  • 988
  • 1
  • 9
  • 19