-1

I'm testing a synchronous middleware stack that I derived from expressjs. For some reason my tests are passing even though the assertions should fail. I know from this question that the .subscribe calls are asynchronous in JEST, but I'm not using any subscribe calls, only expect. I believe that the issue has to do with the test finishing before all of the expect() calls complete. But given the implementation, I'm not sure how that's possible.

test( "Stack proceeds to next error-handling layer if next(error) is explicitly called", () => {
    const S = new Stack();

    S.use( () => {
        throw new Error( "FAIL" );
    });

    S.use( ( err, data, next ) => {
        console.log( err.message) // Prints "FAIL"
        expect( err.message ).toBe( "PASS" ); // Always passes no matter what
        next( err )
    });

    let SKIP_TEST = "PASS";
    S.use( ( data, next ) => {
        SKIP_TEST = "FAIL";
    });

    let CALL_TEST = "FAIL";
    S.use( ( err, data, next ) => {
        CALL_TEST = "PASS";
        // adding a console.log( err.message ) here causes this test to fail, which is the expected behavior. Without a console.log on this line, the test will always pass
        expect( err.message ).toBe( "PASS" );
    });


    S.dispatch(); // Causes the above functions to be called synchronously, in the order they were defined

    expect( SKIP_TEST ).toBe( "PASS" ); // Passes
    expect( CALL_TEST ).toBe( "PASS" ); // Passes
});

I know that the functions passed to .use are being called because I can console.log from them and the output shows up in the console. To make things even more strange, adding a console.log call right before the expect in the final S.use function causes the assertion to (correctly) fail.

Here is the middleware stack that I derived from express.js

"use strict"

export default class Stack {
    constructor(){
        this.stack = [];
    }

    dispatch( data, done = ()=>{} ){
        let idx = 0;
        let stack = this.stack;
        if( stack.length === 0 )
            done();

        next();

        function next( err ){
            let layer = stack[ idx++ ];
            if( !layer )
                return done( err );
            if( err )
                layer.handleError( err, data, next );
            else
                layer.handleNext( data, next );
        }
    }

    use( fn ){
        this.stack.push( new Layer( fn ) );
    }

}

class Layer {
    constructor( fn ){
        // if( !this instanceof Layer )
        //      return new Layer( fn );
        this.handle = fn;
        this.name = fn.name || "<anonymous>";
    }

    handleError( err, data, next ){
        let fn = this.handle;

        // If this layer is not an error-handler, pass error to next layer
        if( fn.length !== 3 )
            return next( err ); // Stop the chain unless next() is explicitly called by the next layer in the stack

        try{
            fn( err, data, next );
        } catch( err ){
            next( err );
        }
    }

    handleNext( data, next ){
        let fn = this.handle;

        // If this layer is not a standard handler, skip this layer
        if( fn.length > 2 )
            return next(); // Stop the chain unless next() is explicitly called by the next layer in the stack

        try{
            fn( data, next );
        } catch( err ) {
            next( err );
        }
    }
}
export{ Stack }

here is a complete reproduction of the issue.

Native Coder
  • 1,792
  • 3
  • 16
  • 34

1 Answers1

0

While I cannot explain why the issue was happening, I managed to work around it by only calling expect() after the dispatch() call. I used a bunch of boolean flags that are set when the corresponding middleware stack is called.

The key is not to call expect(...) inside of a function that is passed as a parameter

This is what the correct test ended up being

test( "MiddlewareStack proceeds to next error-handling layer if 
next(error) is explicitly called", function() {
    const S = new MiddlewareStack();

    // Test flags
    let STD_HANDLER_WAS_CALLED = false;
    let FIRST_ERR_HANDLER_RECEIVED_ERROR = false;
    let FIRST_ERR_HANDLER_RECEIVED_CORRECT_ERROR = false;
    let SECOND_ERR_HANDLER_RECEIVED_ERROR = false;
    let SECOND_ERR_HANDLER_RECEIVED_CORRECT_ERROR = false;
    let SECOND_ERR_HANDLER_WAS_CALLED = false;

    S.use( () => {
        throw new Error( "PASS" );
    });

    S.use( ( err, data, next ) => {
        if( err ){
            FIRST_ERR_HANDLER_RECEIVED_ERROR = true;
            if( err.message === "PASS" )
                FIRST_ERR_HANDLER_RECEIVED_CORRECT_ERROR = true;
        }
        next( err )
    });

    S.use( ( data, next ) => {
        STD_HANDLER_WAS_CALLED = true;
    });

    S.use( ( err, data, next ) => {
        SECOND_ERR_HANDLER_WAS_CALLED = true;
        if( err ){
            SECOND_ERR_HANDLER_RECEIVED_ERROR = true;
            if( err.message === "PASS" )
                SECOND_ERR_HANDLER_RECEIVED_CORRECT_ERROR = true;
        }
    });

    S.dispatch();
    expect( FIRST_ERR_HANDLER_RECEIVED_ERROR ).toBe( true );
    expect( FIRST_ERR_HANDLER_RECEIVED_CORRECT_ERROR ).toBe( true );
    expect( STD_HANDLER_WAS_CALLED ).toBe( false );
    expect( SECOND_ERR_HANDLER_WAS_CALLED ).toBe( true );
    expect( SECOND_ERR_HANDLER_RECEIVED_ERROR ).toBe( true );
    expect( SECOND_ERR_HANDLER_RECEIVED_CORRECT_ERROR ).toBe( true );
});
Native Coder
  • 1,792
  • 3
  • 16
  • 34