How do I stub a chain of methods in Sinon?

2020-06-09 14:41发布

问题:

I know how to use stub to replace one function.

sandbox.stub(Cars, "findOne",
            () => {return car1 });

But now I have a line in my function I want to test that I need to stub that looks like this

Cars.find().fetch()

So there is a chain of function here and I'm unsure what I need to do. How do I stub "find" to return something that I can use to stub "fetch"?

回答1:

Try this:

sandbox.stub(Cars, "find", () => {
    return {
        fetch: sinon.stub().returns(anything);
    };
});


回答2:

IMHO, we can just use returns to do this. We don't need to use callsFake or mock it as function.

// Cars.find().fetch()

sinon.stub(Cars, 'find').returns({
  fetch: sinon.stub().returns(anything);
});

in case, if there is another method after fetch(), we can use returnsThis()

// Cars.find().fetch().where()

sinon.stub(Cars, 'find').returns({
  fetch: sinon.stub().returnsThis(),
  where: sinon.stub().returns(anything);
});

Ref: https://sinonjs.org/releases/v6.3.3/

Hope it helps



回答3:

The form of attaching a function to a stub shown here:

sandbox.stub(Cars, "find", () => {
    return {
        fetch: sinon.stub().returns(anything);
    };
});

is deprecated.

It's now, as of version 6.3

sandbox.stub(Cars, "find").callsFake(() => {
    return {
        fetch: sinon.stub().returns(anything);
    };
});


回答4:

I ran into this problem and, though I liked the solution for a single test, wanted something more dynamic that would allow for reuse across tests. I also preferred the sandbox approach, as it made restoring much easier for larger suites. End result:

export function setupChainedMethodStub(sandbox: sinon.SinonSandbox, obj: any, methodName: string, methodChain: string[], value: any) {
    return sandbox.stub(obj, methodName).returns(generateReturns(sandbox, methodChain, value));
}

function generateReturns(sandbox: sinon.SinonSandbox, methodChain: string[], value: any): any {
    if (methodChain.length === 1) {
        return {
            [methodChain[0]]: sandbox.stub().returns(value),
        };
    } else {
        return {
            [methodChain[0]]: sandbox.stub().returns(generateReturns(sandbox, methodChain.slice(1), value)),
        };
    }
}

Wherever I want to set up a stub on the fly, I pass in the created sandbox and the other parameters:

setupChainedMethodStub(sandbox, MyMongooseModel, 'findOne', ['sort', 'exec'], { foo: 'bar' })

Then I just have a sandbox.restore() in my highest scoped afterEach()



回答5:

This is another approach that also allows spying on chains of jQuery methods - which took me a long time to figure out.

In the example, I am trying to test that an email field is cleared out

    //set up stub and spy
    const valSpy = sandbox.spy();
    const jQueryStub = sandbox
      .stub($.prototype, "find")       // this prototype is important
      .withArgs("input[name=email]")
      .returns({ val: valSpy });

    // call function under test
    learnerAlreadyAccepted(inviteDoc);

    // check expectations
    expect(jQueryStub).to.have.been.called;      // not really necessary
    expect(valSpy).to.have.been.calledWith("");

and the function under test is (roughly):

  learnerAlreadyAccepted = function(doc) {
    $("form").find("input[name=email]").val("");
  }