Diskdrive
Diskdrive

Reputation: 18825

How do I stub a chain of methods in Sinon?

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"?

Upvotes: 22

Views: 13410

Answers (7)

deerawan
deerawan

Reputation: 8443

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

Upvotes: 32

Luis
Luis

Reputation: 5914

There are a few changes from v2.0.

More details here

One of them is:

stub(obj, 'meth', fn) has been removed, see documentation

You can downgrade but I would not recommend it, instead you can do something like this:

let stub = sinon.stub(obj, "meth").callsFake(() => {
  return {
    meth2: sinon.stub().callsFake(() => {
      return {
        meth3: sinon.stub().returns(yourFixture),
      };
    }),
  };
});

Upvotes: 1

digiwand
digiwand

Reputation: 1348

I have a simple solution that hopefully works for others.

Presuming that fetch is also a method on Cars, and fetch and find support method chaining, Cars may look something like this:

class Cars {
  fetch() {
    // do stuff
    return this;
  }

  find() {
    // do stuff
    return this;
  }
}

[ANSWER] We should be able to support method chaining with the stub like this:

  sandbox.stub(Cars, 'fetch').callsFake(function () { return this; }); // optional
  sandbox.stub(Cars, 'findOne').callsFake(function () { return this; });

Upvotes: 0

Goeff
Goeff

Reputation: 130

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()

Upvotes: 1

Andrew Stover
Andrew Stover

Reputation: 301

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);
    };
});

Upvotes: 4

charlesdeb
charlesdeb

Reputation: 601

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("");
  }

Upvotes: 1

aprok
aprok

Reputation: 1157

Try this:

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

Upvotes: 13

Related Questions