简体   繁体   中英

How to handle nested asynchronous operation in unit tests

I have a Javascript module, which is accessing Promise object from another module and then transforming that for it's own use. I am using Bluebird library which ensures, that all promise handlers are called asynchronously. That's quite problem for the testing, especially when that internal promise is not exposed.

module.exports = (testedModule, app) ->
    app.module('otherModule').somePromise.then transformValue

transformValue = (val) ->
    return new Extra.TransformedValue(val)

In the tests, I am mocking that first promise, so I have access to that. Second promise stays inside the module and I don't want to expose it just for the sake of the tests. Note that I am using Mocha+Chai+Sinon.

beforeEach ->
    @initModule = -> app.module('testedModule', testedModule)  # prepare function to initialize tested module
    @dfd = dfd = Promise.defer() # defer promise resolution to tests
    app.module 'otherModule', (otherModule) ->
        otherModule.somePromise = dfd.promise

    @transformSpy = sinon.spy Extra, 'TransformedValue'  # spy on the constructor function
    @promiseTransform = dfd.promise.then =>
        # this usually fails as the spy is called more then once due to async nature of the tests
        @transformSpy.should.have.been.calledOnce
        # promise gets resolved with the return value of the spy
        # thus it should contain created instance of the TransformedValue
        return @transformSpy.firstCall.returnValue

afterEach ->
    @transformSpy.restore()

Some preparations for each test. Simply there is promiseTransform , that gets resolved using dfd.resolve() in each test separately. However the transformSpy itself is attached to global object which is shared by all tests (maybe that should be stubbed too). Most of the tests looks like this:

it 'should test whatever...', ->
    @init() # initialize module
    # something else is tested here, doesn't matter
    # note that @dfd is not resolved here, thus transformValue is not called yet

That works just fine, but then comes the test that actually resolves dfd and everything gets messy here. Sometimes spy is resolved more than once or not at all. It's very confusing race of async operations.

it 'should instantiate TransformedValue with correct argument', (done) ->
    expected = {foo: "bar"}
    @promiseTransform.then (transformed) =>
        # test here that TransformedValue constructor has been called once
        # ... it actually FAILS, because it was called twice already!
        @transformSpy.withArgs(expected).should.have.been.calledOnce
        transformed.should.be.an.instanceof Extra.TransformedValue
    # somehow this resolves promise for previous test and 
    # it causes `transformValue` to be called back there
    @dfd.resolve expected 
    @init()

I have spent like 2 days on this and it's driving me nuts already. Tests should be a tool and the actual code to create. There is probably some obvious solution I am missing out.

Do you have any general (or concrete) tips how to handle this with less confusion and more control and determinism? Currently I am thinking about stubbing whole Promise to actually make it synchronous. But it seems to me, that it sort of invalidates the tests, because the workflow can be different than in real run.

What's with the spies? You wouldn't use spies if this was synchronous code. If everything was synchronous, how would you write the test?

Why not write the test as:

it('should instantiate TransformedValue with correct argument', function() {
    var expected = {};
    return transform(expected).then(function(val) {
        assert.deepEqual(Extra.TransformedValue.value, expected)
        assert(val instanceof Extra.TransformedValue);
    });
});

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM