DEV Community

Cover image for How to use stubs with Cypress
dcodes
dcodes

Posted on • Edited on • Originally published at dawsoncodes.com

How to use stubs with Cypress

Cypress is a great tool for integration tests, unit tests and end-to-end tests for your front-end applications. It provides many features that make it easy to use and reliable. Cypress is also easy to install and test code.

It has a wide range of plugins that can be used to extend its functionality. It also has a great community that can help you get started with using cypress.

What is stubbing in testing applications?

A stub is a piece of code that is used to stand in for another piece of code that is not yet available or is not possible to give you the desired outcome that you want.

For example, if a testing suite needs to make a call to an external API, but the API has not yet been built, the testing team can use a stub to mimic the functionality of the API.

Or if you are calling your API you need actual data in your database which is not available at all times and you still want to get back a favourable response from the API, using the real API, in this case, would be very inconvenient and that's when stubbing comes in.

This allows the testing suite to continue running without error, and also provides a way to assess how the system will behave when the real API is integrated.

While stubbing can be useful in some scenarios, it should not be overused, as it can lead to tests that are not realistic. In general, it is best to avoid stubbing when possible and only use it when necessary.

Why use stubs in your applications?

The stub function in Cypress is useful when you want to test unique scenarios in your application and make sure your application works as expected in every scenario and with various datasets that your application might get.

For example, if you want to retrieve some data from your server from the browser, in order to do that, you have to actually have some data in your database which is not always a guarantee that this data is available.

It might be available on your end, but it might not be available when you run your tests in the cloud or when you hand it over to a partner, with stubs, you always make sure that your tests run everywhere without depending heavily on the APIs.

Stubs can also help to speed up your test processes and help you avoid slow tests. since your application is no longer waiting for the server to process the data and send it back to the client.

Another case of using stubs in Cypress is sometimes your tests depend on your application's date and time, this causes inconvenience for your tests since it makes your application time dependent.

Instead, you can use the cy.clock() function which helps you control the time of your browser and skip through time.

Ways to do stubbing in Cypress

There are multiple functions that Cypress provides to stub other functions, the most important ones are the following:

  • cy.stub() : replaces a function, and controls its behavior.
  • cy.intercept(): Spy and stub network requests and responses.
  • cy.spy(): To wrap a function in a spy, use the cy.spy() command.
  • cy.clock(): To control time in the browser.

cy.stub() is useful for stubbing any function or method inside your application for example: disabling the prompt function on the window object.

cy.intercept is when you want to change the requests and response data of the networks between your browser and your APIs directly.

cy.spy() lets you wrap function and record invocations of that function without actually replacing the original function.

cy.clock is useful to take control of the time of the browser, for example instead of waiting for a counter to finish, you can skip ahead of time and avoid unnecessary waiting in your tests.

The main difference between cy.spy() and cy.stub() is that cy.spy() does not replace the method, it only wraps it. So, while invocations are recorded, the original function is still called without being modified.

This can be very useful when testing methods on native browser objects. You can verify a method is being called by your test and still have the original function action invoked.

Examples of using stubs spies and clocks

In this blog post, I will provide you with some examples and use cases for stubs, spies, and clocks in Cypress.

Stubbing API calls in Cypress with cy.intercept()

If you are using a modern front-end framework for your applications, it is more likely that you are using client-side rendering.

So, instead of the server getting all of the data and generating the page, your application utilizes the browser to fetch all of the data that your application requires, we can use Cypress to manipulate these requests that go to your server by changing the request and response properties.

In order to do that, we will use the cy.intercept() command of Cypress.

For this example, I have created a small to-do app that fetches data from JSON placeholder API.

This is how it looks.

Todo application sample

All of the to-do data is coming from the JSON placeholder API.

Let's say we want to stub the response that we are getting back from the API and get back a custom response in the body of the response.

Let's say you only want to get back two entries from the APIs directly.

In order to do that, I will create a JSON file inside the fixtures folder that cypress provides, and insert the data there.

Fixtures are static sets of data in a located file that you can use throughout your tests.

The fixture cypress/fixtures/todos.json

[
    {
        "userId": 1,
        "id": 1,
        "title": "First todo",
        "completed": false
    },
    {
        "userId": 1,
        "id": 2,
        "title": "Second todo",
        "completed": false
    }
]
Enter fullscreen mode Exit fullscreen mode

The spec file

describe("My todo app", () => {
  it("Gets back only two entries", () => {
    cy.intercept("https://jsonplaceholder.typicode.com/todos*", {
      fixture: "todos.json",
    })
    cy.visit("/")

    cy.get("[data-cy=todo-item]").should("have.length", 2)
  })
})
Enter fullscreen mode Exit fullscreen mode

Notice that we have put a * after the complete URL, this is that for all the different query parameters this interception is being made.

And also make sure that you put the cy.intercept() before the cy.visit() because sometimes the request is being sent without the intercept function finishing completely, therefore making your test fail.

You can also change the method of the request by typing the request method in the first parameter, for example:

cy.intercept("GET", "https://jsonplaceholder.typicode.com/todos*", {
  fixture: "todos.json",
})
Enter fullscreen mode Exit fullscreen mode

If you don't want to use a fixture, you can edit the interception by providing the body with a value.

cy.intercept("GET", "https://jsonplaceholder.typicode.com/todos*", {
  body: [{ title: "First" }, { title: "Second" }],
})
Enter fullscreen mode Exit fullscreen mode

Stubbed applications network requests

You can also change other properties of the request like headers

cy.intercept("https://jsonplaceholder.typicode.com/todos*", {
  body: [{ title: "First" }, { title: "Second" }],
  headers: {
    Authorization: "Bearer <token>",
  },
})
Enter fullscreen mode Exit fullscreen mode

Stubbing functions and properties using the cy.stub() command

Cypress lets you stub functions and properties by using the cy.stub() command.

The function takes two arguments, one of them is the object and the second argument is the method you want to stub.

How to use stub in cypress

An example of using cy.stub() in Cypress

describe("Stubbing", () => {
  const obj = {
    sum: (a: number, b: number) => a + b,
  }

  it("should stub the function", () => {
    cy.stub(obj, "sum").returns(3) // returns 3 instead of a + b

    expect(obj.sum(10, 20)).to.equal(3)
  })
})
Enter fullscreen mode Exit fullscreen mode

As you can see the function will always return 3 no matter what are the inputs.

Stubbing a function based on call count

Sometimes we want to change the returning values of a function based on the call count of that function.

If you want to get some value back for the first function call and get back a different value for the second and third call, you can achieve this by using the onFirstCall(), onSecondCall() and onThirdCall() methods.

it("should stub the function for the first 3 calls", () => {
    cy.stub(obj, "sum")
        .onFirstCall()
        .returns(3)
        .onSecondCall()
        .returns(10)
        .onThirdCall()
        .returns(20)

    expect(obj.sum(10, 20)).to.equal(3)
    expect(obj.sum(10, 20)).to.equal(10)
    expect(obj.sum(10, 20)).to.equal(20)
})
Enter fullscreen mode Exit fullscreen mode

The onFirstCall() method lets you modify the function only for the first call, the onSecondCall() and onThirdCall() also do the same thing for the second and third function call.

Restore a stubbed method

Sometimes you only want to stub your method only once and restore it to the original method. once ran the specific amounts of times that you wanted.

Storing the stub inside of a variable could also be useful so that later in the code you can restore it to the original method.

it("should stub and restore the function", () => {
    const stub = cy
        .stub(obj, "sum")
        .onFirstCall()
        .returns(3)
        .onSecondCall()
        .returns(10)
        .onThirdCall()
        .returns(20)

    expect(obj.sum(10, 20)).to.equal(3)
    expect(obj.sum(10, 20)).to.equal(10)
    expect(obj.sum(10, 20)).to.equal(20)

    stub.restore()

    expect(obj.sum(10, 20)).to.equal(30)
})
Enter fullscreen mode Exit fullscreen mode

using restore function in Cypress.

Stub a property

Stubbing is not only for methods, you can actually stub properties of objects and change their values by using the value() method.

describe("Stub a property", () => {
  const car = {
    color: "red",
    getColor() {
      return this.color
    },
  }

  it("should stub a property", () => {
    expect(car.getColor()).to.equal("red")

    cy.stub(car, "color").value("blue")

    expect(car.getColor()).to.equal("blue")
  })
})
Enter fullscreen mode Exit fullscreen mode

You can also change the value to an object or array or any other data type.

describe("Stub a property", () => {
  const car = {
    color: "red",
    getColor() {
      return this.color
    },
  }

  it("should stub a property", () => {
    expect(car.getColor()).to.equal("red")

    cy.stub(car, "color").value({ message: "blue" })

    expect(car.getColor()).to.deep.equal({ message: "blue" })
  })
})
Enter fullscreen mode Exit fullscreen mode

Cypress app actions and stubs

We can easily stub methods and functions directly inside our application with Cypress.

How do Cypress app actions work?

Because Cypress architecture allows interaction with the application under test, this is simple. All we need to do is to expose a reference to the application's model object by attaching it to the window object.

For example, you can write some JavaScript like this inside of your application

  if (window.Cypress) {
    window.actions.myMethod = myMethod
  }
Enter fullscreen mode Exit fullscreen mode

This way you can use this inside of your Cypress tests

cy.window().its("actions").invoke("myMethod")
Enter fullscreen mode Exit fullscreen mode

Since app actions can easily be used inside of Cypress tests, we can easily stub them and change their return value.

it("should stub a method with app actions", () => {
  cy.visit("/")
  cy.window()
    .its("actions")
    .then((actions) => {
      cy.stub(actions, "myMethod").returns(20)
    })

  cy.window().its("actions").invoke("myMethod").should("eq", 20)
})

Enter fullscreen mode Exit fullscreen mode

Learn about app actions here.

Write your own logic in a stubbed method

Sometimes you don't just want to change the return value of a function or method, what you want is to change some of the logic of the function.

You can do this by calling the callsFake() method.

it("should stub a method with app actions", () => {
  cy.visit("/")
  cy.window()
    .its("actions")
    .then((actions) => {
      cy.stub(actions, "myMethod").callsFake(() => {
        // some logic
        return "Output with logic"
      })
    })

  cy.window()
    .its("actions")
    .invoke("myMethod")
    .should("eq", "Output with logic")
})
Enter fullscreen mode Exit fullscreen mode

cypress callsFake method

Using Spy with Cypress

The spy function is useful by letting you know that a function was called with the right arguments or the function's call count, or to determine what was the return value of the spied function.

A spy does not modify the behaviour of the function - it is left perfectly intact. A spy is most useful when you are testing the contract between multiple functions and you don't care about the side effects the real function may create (if any).

Difference between spies and stubs

A spy is used to test how a particular piece of code is used. Spies are typically used to verify that a function is being called with the correct arguments, or that a callback is being executed as expected.

A stub, on the other hand, is used to replace the behaviour of a particular piece of code. Unlike spies, stubs don't care how the code they're replacing is used - they only care about providing the expected output.

Example of using Spy with Cypress

describe("Using Spy", () => {
  const obj = {
    color: "red",
    getColor() {
      return this.color
    },
    setColor(color: string) {
      this.color = color
    },
  }

  it("should spy the function", () => {
    cy.spy(obj, "getColor")

    obj.getColor()

    expect(obj.getColor).to.have.been.called

    cy.spy(obj, "setColor")

    obj.setColor("blue")

    expect(obj.setColor).to.have.been.calledWith("blue")

    obj.getColor()

    expect(obj.getColor).to.have.returned("red")
  })
})
Enter fullscreen mode Exit fullscreen mode

unit test cypress

In this example, we are making sure that the function getColor() has been called at least one time.

Also, we are making sure that the function setColor() has been used with the correct arguments.

We are also making sure that the function getColor() returns the right values.

If you don't run the function, your assertion will fail.

it("should spy the function", () => {
    cy.spy(obj, "getColor")

    // failing test on purpose
    expect(obj.getColor).to.been.called
})
Enter fullscreen mode Exit fullscreen mode

fail test, test failed, cypress

Manipulating time with clock() and tick() in Cypress

Let's say we have a text and we want to make sure that the end-use will read that text, in order to do that, we will create a simple timer and make the user stop for 10 seconds and then let them in.

This might be useful for your application but it doesn't make any sense to make the application wait when it is being tested.

In order to control the browser's time, use the cy.clock() command for initialization and use the cy.tick() command to skip through time.

The cy.tick() command accepts an argument which is the amount of time to pass in milliseconds.

describe("My todo app", () => {
  it("Should skip through time", () => {
    cy.clock(new Date())
    cy.visit("/")
    cy.tick(10000) // ten seconds
  })
})
Enter fullscreen mode Exit fullscreen mode

This way our tests will not necessarily wait.

The browsers clock is different from the specs clock

Note that using the cy.clock() command will only change the application's clock and not the specs clock which is outside of the application.

it("should not have the same time as the spec's time", () => {
  cy.clock(new Date(Date.UTC(2022, 8, 20)), ["Date"])
  cy.visit("/")
  cy.window().its("Date").invoke("now").should("not.equal", Date.now())
})
Enter fullscreen mode Exit fullscreen mode

cypress command log

As you can see, the application's date is totally different from the specs date and time.

Conclusion

We’ve explored Cypress and how it can be used to create stubs for functions and API calls, spies and clocks. We’ve also looked at some code examples to help you get started. If you found this blog post valuable, don't forget to share it with your colleagues and friends.

Do you have any notes? let us know down below πŸ‘‡

Top comments (4)

Collapse
 
priteshusadadiya profile image
Pritesh Usadadiya

[[..PingBack..]]
This article is curated as a part of #70th Issue of Software Testing Notes Newsletter.

Collapse
 
dcodes profile image
dcodes

Thank you very much.

Collapse
 
rafaelbomfim profile image
Rafael Bomfim

Excellent! Keep bringing Cypress content, I really appreciate it!

Collapse
 
dcodes profile image
dcodes

I'm glad you found it useful.

Surely, I will write more content about Cypress.