ifandelse / machina.js

js ex machina - finite state machines in JavaScript
http://machina-js.org/
Other
1.93k stars 147 forks source link

how to test async component inside a state #135

Closed ysyyork closed 6 years ago

ysyyork commented 7 years ago

Problem description

I have a fsm that when it enters state A, it will make a call to backend server and then transition to a new state. The backend call is async. I want to test this flow. I mocked backend call and want to assert the transition method being called after the call returns. But the test won't wait until the call gets back. So it will complain that transition is not called.

Typically to test async functions, I will use done() passed in as a callback function argument. But in this case, since I write the whole function inside the state. I don't know how to pass in argument. Do you have any suggestions in this case?

Below is a simplified version of my code:

const fsm = new machina.Fsm({
    states: {
        A: {
            _onEnter: function() {
                backend.call(parameters).then((data) => {
                    if (isValid(data)) {
                        this.transition('B');
                    }
                });
            }
        },
        B: function() {
        }
    }
});

Thank you so much

ifandelse commented 6 years ago

@ysyyork I apologize for the long delay (see #146). Since we can't get a handle to the promise (and thus have the test runner hook in and wait on it to resolve), there are at least two other options that, while they're not as clean as returning the promise in a beforeEach or using a done callback, they still work:

(forgive me - this is psuedo code I've pulled out of thin air)

Depending on the state transition

describe( "when moving to B state" , () => {
    let transitionArgs, backend, parameters;
    beforeEach( ( done ) => {
        backend = { 
            call: sinon.stub().resolves( { fakeValidData: true } ) 
        };
        parameters = { whateverMockParamsYouNeed: true };
        const fsm = new machina.Fsm( {
            eventListeners: {
                transitioned: [ 
                    ( { fromState, toState } ) => { 
                        transitionArgs = [ fromState, toState ];
                        done();
                    }
                ]
            },
            states: {
                A: {
                    _onEnter: function() {
                        backend.call( parameters ).then( data => {
                            if ( isValid( data ) ) {
                                this.transition( "B" );
                            }
                        } );
                    }
                },
                B: {}
            }
        } );
    } );

    it( "should call the backend", () => {
        backend.call.should.be.calledOnce.and.calledWith( parameters );
    } );

    it( "should transition to B state", () => {
        transitionArgs.should.eql( [ "A", "B" ] );
    } );
} );

Using a synchronous mock of the promise

describe( "when moving to B state" , () => {
    let thenableStub, backend, parameters, fsm;
    beforeEach( ( done ) => {
        thenableStub = {
            then: sinon.stub().callsArgWith( 0, { fakeValidData: true } )
        };
        backend = { 
            call: sinon.stub().returns( thenableStub ) 
        };
        parameters = { whateverMockParamsYouNeed: true };
        fsm = new machina.Fsm( {
            states: {
                A: {
                    _onEnter: function() {
                        backend.call( parameters ).then( data => {
                            if ( isValid( data ) ) {
                                this.transition( "B" );
                            }
                        } );
                    }
                },
                B: {}
            }
        } );
    } );

    it( "should call the backend", () => {
        backend.call.should.be.calledOnce.and.calledWith( parameters );
    } );

    it( "should transition to B state", () => {
        fsm.state.should.equal( "B" );
    } );
} );

In case you're not familiar with some of the test assertion calls, it's worth checking out these libs: