w3resource

Stubs, Spies, and Clocks


Aim

The aim of this tutorial is to show you how stubs, spies and clocks work in Cypress. We will show you the libraries that Cypress includes to provide typical testing functionality. We will also show you how to use stubs to assert that a code was code but we also want to prevent it from executing. This tutorial will show you how to use spies to assert a code without interfering with its execution. We will also cover how you can control time for deterministically testing code that is time-dependent. Finally, we will examine how Cypress will improve and extend the included libraries.

Capabilities

By default, Cypress comes with the ability to stub and spy with the cy.stub() and cy.spy() commands. It also provides you with the ability to modify your applications time using the cy.clock() command (this command enables you to manipulate date, setTimeout, clearTimeout, setInterval or clearInterval.

These commands are very useful when you are writing both unit tests and integration tests.

Libraries and Tools

The libraries that ae bundled and wrapped automatically with Cypress are tabulated below.

Name What it does
sinon This library provides the cy.stub() and cy.spy() APIs
lolex This library provides the cy.clock() and cy.tick() APIs
sinon-chai This library adds chai assertions for stubs and spies

Common Scenarios

Stubs

A stub refers to a way of modifying a function and delegating controls over its behavior to you.

This is commonly used in a unit test, however, it is still useful in integration and e2e tests.

'''// creates a standalone stub (generally for use in unit test)
cy.stub()

// replaces obj.method() with a stubbed function
cy.stub(obj, 'method')

// forces obj.method() to return "foo"
cy.stub(obj, 'method').returns('foo')

// forces obj.method() when called with "bar" argument to return "foo"
cy.stub(obj, 'method').withArgs('bar').returns('foo')

// forces obj.method() to return a promise which resolves to "foo"
cy.stub(obj, 'method').resolves('foo')

// forces obj.method() to return a promise rejected with an error
cy.stub(obj, 'method').rejects(new Error('foo'))'''

In general, you stub a function when it has side effects that you are trying to control.

Common Scenarios:

  • When you have a function that accepts a callback, and you want to invoke the callback.
  • Your function returns a Promise, and you want to resolve or reject it automatically.
  • You have a function that wraps window.location and you do not want your application to be navigated.
  • You are trying to test your application’s “failure path” by forcing things to fail.
  • You are trying to test your application’s “happy path” by forcing things to pass.
  • You want to “trick” your application into thinking that it is logged in or logged out.
  • You are using oauth and you want to stub login methods.

Spies

A spy will give you the ability to spy on a function, by permitting you to capture and assert that a function was called with the right arguments, or you want to assert that the function was called a certain number of times, or you even want to assert what the return value was or the context the function was called with.

A spy will not modify the behavior of the function- it will be left perfectly intact. A spy is very useful when you are testing the contract between multiple functions and you do not care about the side effects that the real function may create.

cy.spy(obj, 'method')

Clock

There are also situations when it is very useful to control the date and time of your application in order to override its behavior or to avoid slow tests.

With cy.clock() you will be able to control:

  • Date
  • setTimeout
  • setInterval

Common Scenarios

A. Control setInterval

  • You are polling something in your application using setInterval and you want to control that.
  • You have throttled or debounced functions that you want to control.

Once you have enabled cy.clock(), you will be able to control time by ticking it ahead by milliseconds.

'''cy.clock()
cy.visit('http://localhost:3333')
cy.get('#search').type('Acme Company')
cy.tick(1000)'''

You can call cy.clock() before you visit your application and Cypress will automatically bind it to the application on the next cy.visit(). Cypress will bind before any timers from your application can be invoked. This will work the same way that cy.server()  and cy.route() do.

Restore the clock

With Cypress, you will be able to restore the clock and enable your application to resume normally without the need to manipulate the native global functions that are related to time. This will be automatically called between tests.

'''cy.clock()
cy.visit('http://localhost:3333')
cy.get('#search').type('Acme Company')
cy.tick(1000)
// more test code here

// restore the clock
cy.clock().then((clock) => {
  clock.restore()
})
// more test code here'''

You can also restore the clock by using the .invoke() to invoke the restore function.

cy.clock().invoke('restore')

Assertions

Once you have a spy or a stub, you can then create assertions about them.

'''const user = {
  getName: (arg) => {
    return arg
  },

  updateEmail: (arg) => {
    return arg
  },

  fail: () => {
    throw new Error('fail whale')
  }
}

// force user.getName() to return "Jane"
cy.stub(user, 'getName').returns('Jane Lane')

// spy on updateEmail but do not change its behavior
cy.spy(user, 'updateEmail')

// spy on fail but do not change its behavior
cy.spy(user, 'fail')

// invoke getName
const name  = user.getName(123)

// invoke updateEmail
const email = user.updateEmail('[email protected]')

try {
  // invoke fail
  user.fail()
} catch (e) {

}

expect(name).to.eq('Jane Lane')                            // true
expect(user.getName).to.be.calledOnce                      // true
expect(user.getName).not.to.be.calledTwice                 // true
expect(user.getName).to.be.calledWith(123)
expect(user.getName).to.be.calledWithExactly(123)          // true
expect(user.getName).to.be.calledOn(user)                  // true

expect(email).to.eq('[email protected]')                       // true
expect(user.updateEmail).to.be.calledWith('[email protected]') // true
expect(user.updateEmail).to.have.returned('[email protected]') // true
expect(user.fail).to.have.thrown('Error')                  // true
'''

Integration and Extensions

Beyond the integration of these tools together, Cypress also extends and improves the collaboration between these tools.

Here are some examples:

  • The Sinon’s argument stringifier has been replaced for a much less noisy, more performant, custom version.
  • The sinon-chai assertion output has been improved by changing what is displayed during a passing vs. failing test.
  • Aliasing support has been added to stub and spy APIs.
  • Cypress automatically restores and tears down stub, spy, and clock between tests.

Also, Cypress has integrated all of these APIs directly into the Command Log, so that you can visually see what is happening in your application.

Cypress will visually indicate when:

  • A stub is called
  • A spy is called
  • A clock is ticked

Whenever you use aliasing with the .as() command, we will also correlate those aliases with the calls. This will work identically to aliasing a cy.route().

When you create stubs by calling the method .withArgs(...) Cypress will also link these together visually.

Whenever you click on a stub or spy, Cypress will also output remarkably helpful debugging information.

For example, Cypress will automatically display:

  • The call count (and total number of calls)
  • The arguments, without transforming them (they are the real arguments)
  • The return value of the function
  • The context the function was invoked with