Search code examples
javascriptmocha.js

Mocha - pass variable to `beforeEach` and `afterEach` from `it`


This question could solve a lot of general solutions, so I'll try to phrase it in such a way. I have a situation where I need the same beforeEach and afterEach block of code run but need to alter just one variable (or a couple) each time.

My specific scenario: I need to insert/delete from a database each time. Code:

First setting up temporary table for tests:

before(function(done) {
    db.query('CREATE TEMPORARY TABLE tmp AS SELECT * FROM example_table', null, function(error) {
        return done();
    });
});

Then hooks before/after each test [EDIT with @luboskrnac suggestions]:

var vals = []

beforeEach(init_vals, function(done) {
    mysql.query('INSERT INTO tmp (a, b) VALUES (?, ?)', init_vals, function(error, rows) {
        return done();
    });
});

// pass values [1, 2] to `beforeEach`
it('check value is in database', function(done) {
    mysql.query('SELECT * FROM tmp WHERE a = ? AND b = ?', function(error, rows) {
        vals = [1, 2];
        expect(rows[0].a === vals.a);
        expect(rows[0].b === vals.b);
        return done(); // pass values
    })
})

afterEach(function(done) {
    mysql.query('DELETE FROM tmp WHERE a = ? AND b = ?', vals, function(error, rows) {
        return done();
    });
});

Then (temporary) table is cleaned up when session closes. Is there anyway to pass values as a variable to these hooks for each it test?


Solution

  • It is not possible for a it call to constrain what data the beforeEach hook that Mocha will run before the it call will see. The it call can affect what runs after it but not what runs before it.

    What I would do is just move the initialization code to a function that can be called from the tests, and call it in each test. Something like this:

    describe(function () {
        // Record values we may need to clean after the test.
        var to_clean;
    
        function init(vals, cb) {
            mysql.query('INSERT INTO tmp (a, b) VALUES (?, ?)', vals,
                        function(error, rows) {
                            // Record that we need to clean some values.
                            to_clean = vals;
                            cb(error);
                        });
        }
    
        it('check value is in database', function(done) {
            var vals = [1, 2];
            init(vals, function (error) {
                if (error) {
                    done(error);
                    return;
                }
    
                mysql.query('SELECT * FROM tmp WHERE a = ? AND b = ?',
                            function(error, rows) {
                                if (error) {
                                    done(error);
                                    return;
                                }
    
                                // You need to catch these if you want a
                                // precise error report from
                                // Mocha. Otherwise, you get a vague
                                // timeout error.
                                try {
                                    expect(rows[0].a === vals.a);
                                    expect(rows[0].b === vals.b);
                                }
                                catch(e) {
                                    done(e);
                                    return;
                                }
    
                                done();
                            });
            });
        });
    
        afterEach(function(done) {
            if (to_clean) {
                mysql.query('DELETE FROM tmp WHERE a = ? AND b = ?', to_clean,
                            function(error, rows) {
                                // Reset so that we don't try to clean
                                // obsolete data. Yes, we clear this even
                                // if there was an error.
                                to_clean = undefined;
                                done(error);
                            });
            }
            else {
                // Nothing to clean, we're done.
                done();
            }
        });
    });
    

    You'll notice I have some additional error checking that was not in your code. This is necessary to ensure that errors are reported by Mocha with a maximum of details. I strongly suggest using a library that returns promises, as it would simplify the code a lot.

    The Difference Between Hooks and Having a Shared Function

    Mocha treats test failures and hook (before, beforeEach, etc.) failures differently. If a test fails, Mocha will continue executing other tests. If a hook fails, Mocha considers this to be a failure of the test suite and will skip executing anything that depends on the hook. Here's a trivial example:

    describe("top", () => {
        let fail = true;
        beforeEach(() => {
            fail = !fail;
            if (fail) {
                throw new Error("fail");
            }
        });
    
        it("one", () => {});
        it("two", () => {});
        it("three", () => {});
    });
    
    describe("top 2", () => {
        it("one", () => {});
        it("two", () => {});
        it("three", () => {});
    });
    

    I get this output when I run Mocha on the above:

      top
        ✓ one
        1) "before each" hook for "two"
    
      top 2
        ✓ one
        ✓ two
        ✓ three
    
    
      4 passing (10ms)
      1 failing
    
      1) top "before each" hook for "two":
         Error: fail
          at Context.beforeEach (test2.js:6:19)
    

    You can see in the first describe block, only the first two tests were run. As soon as beforeEach fails, Mocha skips the rest of the tests in the describe block, but the 2nd one can be run because it does not depend on the failed hook.

    So when you use a shared function rather than a hook to initialize data for a test, you are essentially causing Mocha to treat initialization failures as test failures rather than suite failures. It is debatable where the line falls between tests failures and suite failures and really depends on the details of the project.