Sinon Edge Cases

How to return difference values depending on the stub's arguments.

Sinon.js is my favorite JavaScript library for spying and stubbing object methods. This blog post describes a few seldomly used stub features that I always have to look up. These examples come from my Cypress examples page.

callThrough

Imagine we want to stub a method, and return a value for the specific argument, but let the original method be called for all other arguments. We can use the callThrough feature.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const greeter = {
greet(name) {
return `Hello, ${name}!`
},
}

const stub = cy.stub(greeter, 'greet')
// all non-matched calls should call the real method
stub.callThrough()
// all calls with a string argument should get "Hi"
stub.withArgs(Cypress.sinon.match.string).returns('Hi')
// call the "greet" method
expect(greeter.greet('World')).to.equal('Hi')
expect(greeter.greet(42)).to.equal('Hello, 42!')

callsFake

To have the absolute power over the stub, I sometimes use the `callsFake Sinon feature to redirect the method calls to my own function.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
const person = {
getName() {
return 'Joe'
},
}

cy.stub(person, 'getName').callsFake(() => {
return (
// call the real person.getName() using the wrappedMethod
person.getName
.wrappedMethod()
// but then reverse the returned string
.split('')
.reverse()
.join('')
)
})
expect(person.getName()).to.equal('eoJ')

It allows me to programmatically decide what to do with the call. In my repo cypress-form-opens-second-tab-example I am stubbing the document.createElement calls when the argument is form, but let the original method be called for all other elements.

1
2
3
4
5
6
7
8
9
10
11
12
13
const create = doc.createElement.bind(doc)
cy.stub(doc, 'createElement').callsFake((name) => {
if (name === 'form') {
const form = create('form')
cy.stub(form, 'target').value('_self')
// Also spy on the instance method "submit"
// so that later we can validate the submitted form
cy.spy(form, 'submit').as('submit')
return form
} else {
return create(name)
}
})

value

You can stub an object's property using the value keyword. For example, to prevent anyone from setting the target property in the above <form> example, I used the following syntax:

1
cy.stub(form, 'target').value('_self')

The application code can do the following:

1
2
const form = document.createElement('form')
form.target = 'test_blank'

Yet, the form element will always submit in the current browser window, because the form.target will always remain _self.

resetHistory

Sinon spies/stubs keep the history of their calls. We can reset the history whenever we want

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// test subject
const person = {
age: 0,
birthday() {
this.age += 1
},
}
// spy on the subject's method
cy.spy(person, 'birthday').as('birthday')
cy.wrap(person)
.its('age')
.should('equal', 0)
.then(() => {
// the application calls the method twice
person.birthday()
person.birthday()
})
// verify the spy recorded two calls
cy.get('@birthday').should('have.been.calledTwice')
cy.get('@birthday').its('callCount').should('equal', 2)
// reset the spy's history
cy.get('@birthday').invoke('resetHistory')
// the spy call count and the history have been cleared
cy.get('@birthday').its('callCount').should('equal', 0)
cy.get('@birthday').should('not.have.been.called')

restore

When you no longer want to use the stub, call .restore() method on the stub

1
2
3
4
5
6
7
8
9
10
11
12
const person = {
getName() {
return 'Joe'
},
}

expect(person.getName(), 'true name').to.equal('Joe')
cy.stub(person, 'getName').returns('Cliff')
expect(person.getName(), 'mock name').to.equal('Cliff')
// restore the original method
person.getName.restore()
expect(person.getName(), 'restored name').to.equal('Joe')

If use have a Cypress alias, you can also invoke the restore method:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const person = {
getName() {
return 'Joe'
},
}

expect(person.getName(), 'true name').to.equal('Joe')
cy.stub(person, 'getName').returns('Cliff').as('getName')
expect(person.getName(), 'mock name').to.equal('Cliff')
cy.get('@getName')
.should('have.been.calledOnce')
.invoke('restore')
.then(() => {
expect(person.getName(), 'restored name').to.equal('Joe')
})

Learn more