2

I'm trying to use QUnit to test a bunch of javascript. My code looks something like this:

module("A");
doExpensiveSetupForModuleA();
asyncTest("A.1", testA1);
asyncTest("A.2", testA3);
asyncTest("A.3", testA3);

module("B");
doExpensiveSetupForModuleB();
asyncTest("B.1", testB1);
asyncTest("B.2", testB3);
asyncTest("B.3", testB3);

If I run this as-is, then doExpensiveSetupForModuleB() runs while the async tests are running, causing failures.

If doExpensiveSetupForModuleB() is run before testA*, then those tests will either fail or undo the expensive setup work so that testB* fails.

Is there a way to have QUnit block on the next module? Or to have it block starting a new test until the previous asynchronous test has completed? Or is there a better framework for JS testing that I should be using?

Note: I understand that my unit tests are not perfectly atomic. I do have cleanup code that helps make sure I don't get any dirty state, but doExpensiveSetupFor*() is prohibitively expensive, such that it wouldn't be realistic to run it before each test.

Hounshell
  • 5,321
  • 4
  • 34
  • 51

3 Answers3

2

Could you use the module lifecycle?

function runOnlyOnce(fn) {
    return function () {
        try {
            if (!fn.executed) {
                fn.apply(this, arguments);
            }
        } finally {
            fn.executed = true;
        }
    }
}

// http://api.qunitjs.com/module/
module("B", {
    setup: runOnlyOnce(doExpensiveSetupForModuleB)
});
Paul Grime
  • 14,970
  • 4
  • 36
  • 58
  • In theory I think I could do that. I've got this odd situation going on where all of my tests are listed in order, but they actually execute in reverse order. I could hack around that, but I'd cringe waiting for the day where the order reverses itself unexpectedly. – Hounshell May 02 '13 at 23:18
  • Good sugggestion, here's another. You don't need to add a magic property `fn.executed`, you can just use a local variable in `runOnlyOnce` See http://stackoverflow.com/questions/3332265/how-to-detect-a-function-was-called-with-javascript/4834482#4834482 It probably won't matter, but you should follow the principle of least privilege and not modify something you didn't create, since there's a potential for name collisions in this case. – Ruan Mendes Apr 18 '14 at 16:03
  • @Hounshell Your tests should be atomic, they should not depend on the order they run, QUnit may run your tests in any order, and the default is to run failing tests first, you can change that but QUnit recommends against that, their recommendation is to fix your test, because if a test is not atomic, it's not a true unit test. Look for QUnit.config.reorder http://api.qunitjs.com/QUnit.config/ – Ruan Mendes Apr 18 '14 at 16:06
1

This is an example, adapted from your original code, that executes the setup method for each test method:

function doExpensiveSetupForModuleA() {
    console.log("setup A");
}

function testA1() {
    console.log("testA1");
    start();
}

function testA2() {
    console.log("testA2");
    start();
}

function testA3() {
    console.log("testA3");
    start();
}

function doExpensiveSetupForModuleB() {
    console.log("setup B");
}

function testB1() {
    console.log("testB1");
    start();
}

function testB2() {
    console.log("testB2");
    start();
}

function testB3() {
    console.log("testB3");
    start();
}

QUnit.module("A", { setup: doExpensiveSetupForModuleA });
    asyncTest("A.1", testA1);
    asyncTest("A.2", testA2);
    asyncTest("A.3", testA3);

QUnit.module("B", { setup: doExpensiveSetupForModuleB });
    asyncTest("B.1", testB1);
    asyncTest("B.2", testB2);
    asyncTest("B.3", testB3);

This will work independent of the order in which the tests are executed and also independent of the time spent by each method to terminate.

The calls to start() will assure that the test results will be collected only in that point of the method.

More detailed examples can be found in the QUnit Cookbook: http://qunitjs.com/cookbook/#asynchronous-callbacks

Updated: if you don't want your expensive methods to be executed before each test method, but actually only once per module, just add control variables to your code to check if the module was already set up:

var moduleAsetUp = false;
var moduleBsetUp = false;

function doExpensiveSetupForModuleA() {
    if (!moduleAsetUp) {
        console.log("setting up module A");
        moduleAsetUp = true;
    }
}

...

function doExpensiveSetupForModuleB() {
    if (!moduleBsetUp) {
        console.log("setting up module B");
        moduleBsetUp = true;
    }
}
...

In this sample, the output would be:

setting up module A 
 testA1 
 testA2 
 testA3 
 setting up module B 
 testB1 
 testB2 
 testB3 

This way you are using your expensive methods as module setup instead of test method setup.

Unit tests are supposed to be atomic, independent, isolated, and thus the order in which they run shouldn't be relevant.

Qunit doesn't always run tests in the same order, anyway, if you want your tests to run in specific order, you can just tell QUnit to don't reorder them:

QUnit.config.reorder = false;

This way you can ensure that testA will run before testB.

jfoliveira
  • 2,138
  • 14
  • 25
  • Sorry, but the expensive setup I refer to needs to be done once before the entire module, not before each test. Think of it as moduleSetup, rather than testSetup. – Hounshell May 13 '13 at 18:08
  • I updated the answer with an example of how to use the method as a moduleSetup. – jfoliveira May 13 '13 at 22:47
  • OK, so how do I ensure that the tests in module A has completed before module B? Generally when I've executed my tests they've run in reverse order. – Hounshell May 14 '13 at 23:03
  • Updated with details about not reordering tests. – jfoliveira May 22 '13 at 08:02
0

I think you have a misunderstanding on how the test declarations work.

QUnit can run any test independently. Just because you declare a test with test() or asyncTest() does NOT mean QUnit will call the function passed in. The "Rerun" links next to each test reload the page and skip every test but the specific one.

So if you want to rerun a B module test, your code will set up A module, even though it does not need to.

The module setup solution posted by others is likely the way to go here.

user169771
  • 1,962
  • 2
  • 13
  • 11