How to test multiple calls to the same function with different params?

Kaizo picture Kaizo · Jan 16, 2014 · Viewed 18.1k times · Source

Supose I have a function like this:

function foo () {
    obj.method(1);
    obj.method(2);
    obj.method(3);
}

To test it I want to do 3 tests (using Mocha TDD and Sinon):

test('medthod is called with 1', function () {
    var expectation = sinon.mock(obj).expects('method').once().withExactArgs(1);
    foo();
    expectation.verify();
});

test('medthod is called with 2', function () {
    var expectation = sinon.mock(obj).expects('method').once().withExactArgs(2);
    foo();
    expectation.verify();
});

test('medthod is called with 3', function () {
    var expectation = sinon.mock(obj).expects('method').once().withExactArgs(3);
    foo();
    expectation.verify();
});

Using this system sinon fails with "unexpected call" message on each test.

I've solved it joining the tree tests into one:

test('medthod is called with 1, 2 and 3', function () {
    var mock = sinon.mock(obj);
    mock.expects('method').once().withExactArgs(1);
    mock.expects('method').once().withExactArgs(2);
    mock.expects('method').once().withExactArgs(3);
    foo();
    mock.verify();
});

But i want to have three tests and not one with three assertions/expectations.

How can this be achieved?

Answer

Kaizo picture Kaizo · Jul 15, 2014

As always, when there is something weird about a test the problem is in the code being tested.

In this case we suffer from coupling. Currently the function has two responsibilities:

  • Decide the data to use.
  • Call the method with the data.

To solve this we must divide the responsibilities in two functions/objects/classes and then test each one separately. For example one possibility could be:

  • The first function (one that generates and returns the data) would be tested checking that the returned data matches our expectations.

  • The second function (our original one) would have a test checking that it calls the data generator, then a test checking that it sends the data correctly to the expected function and a third one checking that it calls the functions as many times as needed by the data.

The code would be something like this:

function foo() {
    dataGenerator.generate().forEach(function (item) {
        obj.method(item);
    })
}

dataGenerator.generate = function () {
    return [1,2,3];
};

And the tests:

test('generateData is called', function () {
    var expectation = sinon.mock(dataGenerator).expects('generate').once();
    foo();
    expectation.verify();
});

test('method is called with the correct args', function () {
    var expectedArgs = 1;
    sinon.stub(dataGenerator, "generate").returns([expectedArgs]);
    var expectation = sinon.mock(obj).expects('method').once().withExactArgs(expectedArgs);
    foo();
    expectation.verify();
});

test('method is called as many times as the amount of data', function () {
    sinon.stub(dataGenerator, "generate").returns([1,2]);
    var expectation = sinon.mock(obj).expects('method').twice();
    foo();
    expectation.verify();
});

test('dataGenerator.generate returns [1,2,3]', function () {
    var expected = [1,2,3];
    var result = dataGenerator.generate();
    assert.equal(result, expected)
});

Note that the third test only checks the amount of times the method is called. The second test has already checked that the data is passed correctly and the fourth tests the data itself.