Spies, Stubs & Clocks

Examples of using stubs, spies, and controlling clock time - for a full reference of commands, go to docs.cypress.ioopen in new window

cy.spy()open in new window

To wrap a method in a spy, use the cy.spy() command.

const obj = {
  foo() {},
}

const spy = cy.spy(obj, 'foo').as('anyArgs')

obj.foo()

// assert against the spy directly
expect(spy).to.be.called
// or get the spy via its reference
cy.get('@anyArgs').should('have.been.called')

Spy retries

cy.spy() retries until the assertions that follow it pass.

const obj2 = {
  foo() {},
}

cy.spy(obj2, 'foo').as('foo')

setTimeout(() => {
  obj2.foo()
}, 500)

setTimeout(() => {
  obj2.foo()
}, 2500)

cy.get('@foo').should('have.been.calledTwice')

Checking the call arguments

You can verify the spy or stub was called with expected arguments.

const person = {
  setName(first, last) {
    this.name = first + ' ' + last
  },
}

cy.spy(person, 'setName').as('setName')
// simulate the application calling the method
// after some random delay
setTimeout(() => {
  person.setName('Joe', 'Smith')
}, Math.random() * 2000)
// check if the spy was called
cy.get('@setName').should('have.been.called')
// check if the spy was called exactly once
cy.get('@setName').should('have.been.calledOnce')
// check if the spy was called with first argument 'Joe'
// the call might have had more arguments, but we do not check them
cy.get('@setName').should('have.been.calledOnceWith', 'Joe')
// check if the spy was called with these arguments
cy.get('@setName').should(
  'have.been.calledOnceWith',
  'Joe',
  'Smith',
)
// check if the spy was called once with a string and "Smith"
cy.get('@setName').should(
  'have.been.calledOnceWith',
  Cypress.sinon.match.string,
  'Smith',
)
// verify the property was set
cy.wrap(person).should('have.property', 'name', 'Joe Smith')

For matching objects in the arguments, see the recipe Spy called with an object.

Matchers

cy.spy and cy.stub match call arguments using Sinon matchersopen in new window.

// see all possible matchers at
// https://sinonjs.org/releases/latest/matchers/
const calculator = {
  /**
   * returns the sum of two arguments
   * @param a {number}
   * @param b {number}
   */
  add(a, b) {
    return a + b
  },
}

const spy = cy.spy(calculator, 'add').as('add')

expect(calculator.add(2, 3)).to.equal(5)

// if we want to assert the exact values used during the call
expect(spy).to.be.calledWith(2, 3)

// let's confirm "add" method was called with two numbers
expect(spy).to.be.calledWith(
  Cypress.sinon.match.number,
  Cypress.sinon.match.number,
)

// alternatively, provide the value to match
expect(spy).to.be.calledWith(
  Cypress.sinon.match(2),
  Cypress.sinon.match(3),
)

// match any value
expect(spy).to.be.calledWith(Cypress.sinon.match.any, 3)

// match any value from a list
expect(spy).to.be.calledWith(
  Cypress.sinon.match.in([1, 2, 3]),
  3,
)

/**
 * Returns true if the given number is event
 * @param {number} x
 */
const isEven = (x) => x % 2 === 0

// expect the value to pass a custom predicate function
// the second argument to "sinon.match(predicate, message)" is
// shown if the predicate does not pass and assertion fails
expect(spy).to.be.calledWith(
  Cypress.sinon.match(isEven, 'isEven'),
  3,
)

/**
 * Returns a function that checks if a given number is larger than the limit
 * @param {number} limit
 * @returns {(x: number) => boolean}
 */
const isGreaterThan = (limit) => (x) => x > limit

/**
 * Returns a function that checks if a given number is less than the limit
 * @param {number} limit
 * @returns {(x: number) => boolean}
 */
const isLessThan = (limit) => (x) => x < limit

// you can combine several matchers using "and", "or"
expect(spy).to.be.calledWith(
  Cypress.sinon.match.number,
  Cypress.sinon
    .match(isGreaterThan(2), '> 2')
    .and(Cypress.sinon.match(isLessThan(4), '< 4')),
)

expect(spy).to.be.calledWith(
  Cypress.sinon.match.number,
  Cypress.sinon
    .match(isGreaterThan(200), '> 200')
    .or(Cypress.sinon.match(3)),
)

// matchers can be used from BDD assertions
cy.get('@add').should(
  'have.been.calledWith',
  Cypress.sinon.match.number,
  Cypress.sinon.match(3),
)

// you can alias matchers for shorter test code
const { match: M } = Cypress.sinon

cy.get('@add').should('have.been.calledWith', M.number, M(3))

Call count

A spy is just an object. You can access and assert its individual properties using .itsopen in new window command.

<button id="greet">Log greeting</button>
<script>
  // note to make things more interesting
  // we will log the message asynchronously
  document
    .getElementById('greet')
    .addEventListener('click', function () {
      setTimeout(function () {
        console.log('Happy Testing!')
      }, 200)
    })
</script>
// first, let's spy on the console.log method
cy.window()
  .its('console')
  .then((console) => cy.spy(console, 'log').as('log'))
// second, act on the UI
cy.get('#greet').click().click().click()
// third assert the spy was called three times
cy.get('@log').its('callCount').should('eq', 3)

Reset call count

You can reset a spy / stub using the .resetHistory method.

// 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.wrap(person)
  .its('age')
  .should('equal', 2)
  .then(() => {
    person.birthday()
    person.birthday()
  })
cy.get('@birthday').its('callCount').should('equal', 4)
cy.log('**reset 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')

Returned value

We can check the value returned by the method.

const calc = {
  add(a, b) {
    return a + b
  },
}
cy.spy(calc, 'add').as('add')
expect(calc.add(4, 5), 'sum').to.equal(9)
cy.get('@add').should('have.returned', 9)
// if there are several calls
calc.add(1, 2)
calc.add(100, 200)
// the assertion will pass if at least one returned 3
cy.get('@add').should('have.returned', 3)

Resolved value

We can check the value returned by the method.

const calc = {
  async add(a, b) {
    const sum = await Cypress.Promise.resolve(a + b).delay(1000)
    return sum
  },
}
cy.spy(calc, 'add').as('add')
// wait for the promise to resolve
// then confirm its resolved value
cy.wrap(calc.add(4, 5)).should('equal', 9)
// make a few more calls
cy.wrap(calc.add(1, 90)).should('equal', 91)
cy.wrap(calc.add(-5, -8)).should('equal', -13)
// example of confirming one of the calls used add(4, 5)
cy.get('@add').should('have.been.calledWith', 4, 5)
// now let's confirm the resolved values
// first we need to wait for all promises to resolve
cy.get('@add')
  .its('returnValues')
  // yields N promises, let's wait for them to resolve
  // in this test they should be resolved already
  // since we used cy.wrap() individually
  .then(Promise.all.bind(Promise))
  // alternative: to avoid the wrong "this" value when using Promise.all
  // .then((ps) => Promise.all(ps))
  .should('deep.equal', [9, 91, -13])

Call the spy from the test

When spying on the object's method typically you call the method. If you do call the created spy function, it does not have the this pointing at the original object. You can bind the this to point to the object by using .bind method.

const testRunner = {
  name: 'Cypress',
  getName() {
    return this.name
  },
}

const getNameSpy = cy.spy(testRunner, 'getName')
// the created spy is just a function
expect(getNameSpy).to.be.a('function')
// call the object's method
expect(testRunner.getName(), 'object.method').to.equal('Cypress')
expect(getNameSpy, 'method was called').to.be.calledOnce
expect(
  getNameSpy.call(testRunner),
  'call the spy directly',
).to.equal('Cypress')
expect(getNameSpy, 'method was called twice').to.be.calledTwice

Watch the video How To Call The Spy Functionopen in new window.

cy.stub()open in new window

To create a stub and/or replace a function with a stub, use the cy.stub() command.

const obj = {
  /**
   * prints both arguments to the console
   * @param a {string}
   * @param b {string}
   */
  foo(a, b) {
    // eslint-disable-next-line no-console
    console.log('a', a, 'b', b)
  },
}

const stub = cy.stub(obj, 'foo')

obj.foo('foo', 'bar')

expect(stub).to.be.called

Return different values for different calls

If you want to return one value on the first call, then another value on the second call, etc

<button id="fav-color">Ask about color</button>
<div id="color-output" />
<script>
  document
    .getElementById('fav-color')
    .addEventListener('click', function () {
      const color = window.prompt('What is your favorite color?')
      document.getElementById('color-output').innerText = color
    })
</script>
cy.window().then((w) => {
  // see how to use Sinon stubs
  // https://sinonjs.org/releases/v10.0.0/stubs/
  cy.stub(w, 'prompt')
    .onFirstCall()
    .returns('green')
    .onSecondCall()
    .returns('red')
    // after the first 2 calls, always return purple
    .returns('purple')
    // give the stub an alias
    // so we can check the number of calls later
    .as('color-stub')
})
cy.get('#fav-color').click()
cy.contains('#color-output', 'green')
cy.get('#fav-color').click()
cy.contains('#color-output', 'red')
cy.get('#fav-color').click().click().click()
cy.contains('#color-output', 'purple')
cy.get('@color-stub').should('have.property', 'callCount', 5)

Return different dynamic values

If you want to return one value on the first call, then another value on the second call, etc and the return values come from an array, you cannot use stub.onFirstCall, stub.onSecondCall, etc. Instead use stub.onCall(k) method

<button id="fav-colors">Ask about color</button>
<div id="colors-output" />
<script>
  document
    .getElementById('fav-colors')
    .addEventListener('click', function () {
      const color = window.prompt('What is your favorite color?')
      document.getElementById('colors-output').innerText = color
    })
</script>
cy.window().then((w) => {
  const colors = ['green', 'red']

  // see how to use Sinon stubs
  // https://sinonjs.org/releases/v10.0.0/stubs/
  const s = cy.stub(w, 'prompt')
  colors.forEach(function (color, k) {
    s.onCall(k).returns(color)
  })
  // after that always return purple
  s.returns('purple')
    // give the stub an alias
    // so we can check the number of calls later
    .as('colors-stub')
})
cy.get('#fav-colors').click()
cy.contains('#colors-output', 'green')
cy.get('#fav-colors').click()
cy.contains('#colors-output', 'red')
cy.get('#fav-colors').click().click().click()
cy.contains('#colors-output', 'purple')
cy.get('@colors-stub').should('have.property', 'callCount', 5)

Saving stub under an alias

const obj = {
  foo() {},
}

cy.stub(obj, 'foo').as('foo')
obj.foo('foo', 'bar')

// access the stub using alias
cy.get('@foo').should('have.been.called')

Restore a stub

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

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

Restore a stub from an alias

You can also restore the original method from its alias

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

Matching stub depending on arguments

// see all possible matchers at
// https://sinonjs.org/releases/latest/matchers/
const greeter = {
  /**
   * Greets a person
   * @param {string} name
   */
  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')
// all calls with a number argument should raise an error
stub
  .withArgs(Cypress.sinon.match.number)
  .throws(new Error('Invalid name'))
// let us confirm the stub is working
// case 1: if we call it with a string argument
// it returns "Hi"
expect(greeter.greet('World')).to.equal('Hi')
// case 2: if we call it with a number argument
// it raises an error
// @ts-ignore
expect(() => greeter.greet(42)).to.throw('Invalid name')
expect(greeter.greet).to.have.been.calledTwice

// case 3: non-matched calls invoke the actual method
// @ts-ignore
expect(greeter.greet()).to.equal('Hello, undefined!')

Watch the video Stub A Method Depending On The Call Argumentsopen in new window.

Read the blog post Lock Down Sinon Stubopen in new window.

Stub a property

If we want to temporarily replace an object's property (not a method), we can use .value

const person = {
  name: 'Joe',
  getName() {
    return this.name
  },
}
expect(person.getName(), 'real name').to.equal('Joe')
// change the property using cy.stub
cy.stub(person, 'name').value('Mike')
cy.then(() => {
  expect(person.getName(), 'stub name').to.equal('Mike')
})

Stub application code

If you want to stub a method deep inside the application's code, access it using App Actions by passing the object reference via window object.

<button id="alerter">Alert me</button>
<script>
  const actions = {
    alertTheUser() {
      alert('📣 you clicked the button')
    },
  }
  document
    .getElementById('alerter')
    // 🛑 it is important to NOT pass the method but call the method on click
    // THIS WILL NOT WORK
    // .addEventListener('click', actions.alertTheUser)
    //
    // ✅ This will work.
    // If the "alertsTheUser" method is replaced in the "actions" object
    // then the stub is called during test
    .addEventListener('click', function () {
      actions.alertTheUser()
    })

  // if we are running inside Cypress test
  // expose the object with methods to be stubbed from tests
  if (window.Cypress) {
    console.log('running inside Cypress')
    window.actions = actions
  } else {
    console.log('not running inside Cypress')
  }
</script>
cy.window()
  .its('actions')
  .then((actions) => {
    cy.stub(actions, 'alertTheUser').as('alerted')
  })
cy.get('#alerter').click()
cy.get('@alerted').should('have.been.called')

Call the original method from the stub

Sometimes you might want to call the original method from the stub.

const person = {
  getName() {
    return 'Joe'
  },
}

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

Check the order of calls

You can check if one stub was called before or after another stub.

const cart = {
  init() {
    return 'initialized'
  },
  finalize() {
    return 'finalized'
  },
  execute() {
    this.init()
    this.finalize()
  },
}
cy.spy(cart, 'init')
cy.spy(cart, 'finalize')
cart.execute()
// confirm the "init" method was called before the "finalize" method
expect(cart.init).to.have.been.calledBefore(cart.finalize)

Stub window.open

If an application tries to open a second tab by calling window.open(url, ...), you must prevent itopen in new window. Let's stub the method open and check the arguments after the call.

<button id="help">Show help</button>
<script>
  document
    .getElementById('help')
    .addEventListener('click', function () {
      open('/help', '_blank')
    })
</script>
cy.window().then((win) => cy.stub(win, 'open').as('open'))
cy.contains('button', 'Show help').click()
// confirm the call arguments
cy.get('@open').should('have.been.calledWith', '/help', '_blank')

Watch the video Stub Window Open Method And Confirm The Call Argumentsopen in new window.

Stub window.alert

If the application is using window.alert to show short messages to the user, you can use cy.stub to intercept those calls and avoid blocking the test runner.

<button id="sayhi">Click me</button>
<script>
  document
    .getElementById('sayhi')
    .addEventListener('click', function () {
      alert('Hello there!')
    })
</script>
cy.window().then((win) => {
  cy.stub(win, 'alert').as('alert')
})
cy.get('#sayhi').click()
cy.get('@alert').should(
  'have.been.calledOnceWith',
  'Hello there!',
)
// the application can trigger the alert several times
cy.get('#sayhi').click().click()
// we can confirm the total number of calls
cy.get('@alert').its('callCount').should('equal', 3)

Stub window.prompt

If the application is asking the user for simple information using window.prompt, you can use cy.stub to respond with a test answer.

<button id="greet-by-name">Greet me</button>
<script>
  document
    .getElementById('greet-by-name')
    .addEventListener('click', function () {
      const name = prompt('What is your name?')
      alert('Hello ' + name)
    })
</script>
cy.window().then((win) => {
  cy.stub(win, 'prompt').returns('Cy')
  cy.stub(win, 'alert').as('alert')
})
cy.get('#greet-by-name').click()
cy.get('@alert').should('have.been.calledOnceWith', 'Hello Cy')

Stub a method and restore

const person = {
  name() {
    return 'Joe'
  },
}
expect(person.name(), 'before stub').to.equal('Joe')
// return "Anna" on the first call
// all calls after that will return "undefined"
cy.stub(person, 'name').onFirstCall().returns('Anna')
expect(person.name(), 'first stubbed call').to.equal('Anna')
expect(person.name(), 'second stubbed call').to.equal(undefined)
person.name.restore()
expect(person.name(), 'restored method').to.equal('Joe')

Note: all Cypress stubs and spies are restored automatically before each test.

Stub and call the argument callback

If the stubbed method expects a callback function to be called, Sinon has a helper for that.

const fs = {
  writeFile(onSuccess, onError) {
    // if writing fails, the app calls "onError"
    onError(new Error('fs.writeFile failed'))
  },
}

Let's stub the fs.writeFile method to simulate an error.

const testError = new Error('A test error')
cy.stub(fs, 'writeFile')
  // we "implement" the same principle in our stub
  // it should call the "onError" function
  // which is the call's argument at position index 1
  .callsArgWith(1, testError)
  .as('writeFile')
// call the application code
fs.writeFile(cy.stub().as('onSuccess'), cy.stub().as('onError'))
// and confirm the expected calls were made
// first check the positive assertion
// then the negative one
cy.get('@onError').should('have.been.calledOnceWith', testError)
cy.get('@onSuccess').should('not.have.been.called')

See another example in the recipe Stub Geolocation.

cy.clock()open in new window

To control time in the browser, use the cy.clock() command.

<div id="clock-div">Click for current time!</div>
<script>
  $('#clock-div').on('click', function (e) {
    let $div = $(e.currentTarget)
    // seconds from the unix epoch
    $div.text(new Date().getTime() / 1000)
  })
</script>
// create the date in UTC so its always the same
// no matter what local timezone the browser is running in
const now = new Date(Date.UTC(2017, 2, 14)).getTime()

cy.clock(now)
cy.get('#clock-div').click().should('have.text', '1489449600')

Changes the application clock only

When running Cypress tests, the tests themselves are outside the application's iframe. When you use cy.clock() command you change the application clock, and not the spec's clock.

Fist, let's show that the two Date constructors are different between the application and the spec iframes.

cy.window().its('Date').should('not.equal', Date)

Now let's confirm that cy.clock() controls the application's Date, but not the spec Date.

const specNow = new Date()
const now = new Date(Date.UTC(2017, 2, 14)).getTime()
cy.clock(now) // sets the application clock and pause time
  .then(() => {
    // spec clock keeps ticking
    const specNow2 = new Date()
    // confirm by comparing the timestamps in milliseconds
    expect(+specNow2, 'spec timestamps').to.be.greaterThan(
      +specNow,
    )
  })
// but the application's time is frozen
cy.window()
  .its('Date')
  .then((appDate) => {
    const appNow = new appDate()
    expect(+appNow, 'application timestamps')
      .to.equal(+now)
      .and.to.equal(1489449600000) // the timestamp in milliseconds
  })
// we can advance the application clock by 5 seconds
cy.tick(5000)
cy.window()
  .its('Date')
  .then((appDate) => {
    const appNow = new appDate()
    expect(
      +appNow,
      'timestamp after 5 synthetic seconds',
    ).to.equal(1489449605000)
  })
  // meanwhile the spec clock only advanced by probably less than 200ms
  .then(() => {
    const specNow3 = new Date()
    expect(
      +specNow3,
      'elapsed on the spec clock',
    ).to.be.lessThan(+specNow + 200)
  })

cy.tick()open in new window

To move time in the browser, use the cy.tick() command.

<div id="tick-div">Click for current time!</div>
<script>
  $('#tick-div').on('click', function (e) {
    let $div = $(e.currentTarget)
    // seconds from the unix epoch
    $div.text(new Date().getTime() / 1000)
  })
</script>
// create the date in UTC so its always the same
// no matter what local timezone the browser is running in
const now = new Date(Date.UTC(2017, 2, 14)).getTime()

cy.clock(now)
cy.get('#tick-div').click().should('have.text', '1489449600')
cy.tick(10000) // 10 seconds passed
cy.get('#tick-div').click().should('have.text', '1489449610')

See also