Search code examples
c#xunitmasstransitautomatonymous

Start State Machine in specific state while testing


I am trying to create unit tests for my state machine to check if each state does what it is supposed to do.

I can bring the state machine into any state by publishing the event that starts the state machine and creating all the variables that I need to reach the desired state.

Simplified state machine:

 public class CheckFilesStateMachine : MassTransitStateMachine<DataState>, ICheckFilesStateMachine
    {
        #region Events
        public Event<IEventCheckFile> CheckFile { get; private set; }
        #endregion

        #region States
        
        public State CheckingForFiles { get; }

        public State ValidatingFiles { get; }

        public State NoFilesFound { get; }

        public State Validated { get; }

        public State FilesProcessed { get; }

        public State ValidationFailed{ get; }

        #endregion

        public DataStateMachine(IDataService dataService)
        {
            InstanceState(x => x.CurrentState);

            Event(() => CheckFile, x => x.CorrelateById(context => context.Message.Id));

            Initially(When(CheckFile).TransitionTo(CheckingForFiles));

            WhenEnter(CheckingForFiles, binder => binder
                .Then(x => x.Instance.Files= dataService.GetFiles())
                .IfElse(x => dataService.HasFiles(),
                        x => x.TransitionTo(ValidatingFiles),
                        x => x.TransitionTo(NoFilesFound))
                .Catch<Exception>(ex => ex
                    .Then(x => Console.WriteLine(x.Exception.Message))));

            WhenEnter(ValidatingFiles, binder => binder
                .IfElse(x => dataService.ValidateFiles(),
                        x => x.TransitionTo(Validated),
                        x => x.TransitionTo(ValidationFailed))
                .Catch<Exception>(ex => ex
                    .Then(x => Console.WriteLine(x.Exception.Message))));

            WhenEnter(NoFilesFound, binder => binder.Then(x => Console.WriteLine("No Files found")).Finalize());
            WhenEnter(Validated, binder => binder.Then(x => Console.WriteLine("Validated")).Finalize());
            WhenEnter(ValidationFailed, binder => binder.Then(x => Console.WriteLine("Validation Failed")).Finalize());

            SetCompletedWhenFinalized();
        }
    }

I wrote some unit tests and I can start the machine (followed this guide from the MassTransit Documentation), but I want to check if the states CheckingForFiles and ValidatingFiles are doing what they are supposed to do independent of each other.

So far, I can mock the IDataService methods to reach the desired state by following the flow of the state machine, but is there a way to start the state machine and just jump to a specific state?

UPDATE

Let's say I have another state in there:

 public class CheckFilesStateMachine : MassTransitStateMachine<DataState>, ICheckFilesStateMachine
    {
        #region Events
        public Event<IEventCheckFile> CheckFile { get; private set; }
        #endregion

        #region States
        
        public State CheckingForFiles { get; }

        public State ValidatingFiles { get; }

        public State DeletingFiles{ get; }

        public State NoFilesFound { get; }

        public State Validated { get; }

        public State FilesProcessed { get; }

        public State ValidationFailed{ get; }

        #endregion

        public DataStateMachine(IDataService dataService)
        {
            InstanceState(x => x.CurrentState);

            Event(() => CheckFile, x => x.CorrelateById(context => context.Message.Id));

            Initially(When(CheckFile).TransitionTo(CheckingForFiles));

            WhenEnter(CheckingForFiles, binder => binder
                .Then(x => x.Instance.Files= dataService.GetFiles())
                .IfElse(x => dataService.HasFiles(),
                        x => x.TransitionTo(ValidatingFiles),
                        x => x.TransitionTo(NoFilesFound))
                .Catch<Exception>(ex => ex
                    .Then(x => Console.WriteLine(x.Exception.Message))));

            WhenEnter(ValidatingFiles, binder => binder
                .IfElse(x => dataService.ValidateFiles(),
                        x => x.TransitionTo(Validated),
                        x => x.TransitionTo(DeletingFiles))
                .Catch<Exception>(ex => ex
                    .Then(x => Console.WriteLine(x.Exception.Message))));

            WhenEnter(DeletingFiles, binder => binder
                .Then(x => x.Instance.Files= dataService.RemoveFiles())
                .TransitionTo(ValidationFailed)
                .Catch<Exception>(ex => ex
                    .Then(x => Console.WriteLine(x.Exception.Message))));

            WhenEnter(NoFilesFound, binder => binder.Then(x => Console.WriteLine("No Files found")).Finalize());
            WhenEnter(Validated, binder => binder.Then(x => Console.WriteLine("Validated")).Finalize());
            WhenEnter(ValidationFailed, binder => binder.Then(x => Console.WriteLine("Validation Failed")).Finalize());

            SetCompletedWhenFinalized();
        }
    }

In this case, can I enter in CheckingForFiles and then jump to DeletingFiles? Is it possible?


Solution

  • If you are using the container-based in-memory test harness, you can resolve the saga dictionary from the container and add an instance:

    var dictionary = provider.GetRequiredService<IndexedSagaDictionary<DataState>>();
    
    dictionary.Add(new SagaInstance<DataState>(new DataState() 
    {
        CorrelationId = dataId,
        CurrentState = "CheckingForFiles" 
    }));
    

    Update

    The latest version of MassTransit, when using the test harness, has an extension method to add a saga instance:

    harness.AddSagaInstance<DataState>(dataId, x => 
    {
        x.CurrentState = "CheckingForFiles";
    });