Back

Mocking and Stubbing in Cypress unit tests

Mocking and Stubbing in Cypress unit tests

Unit testing forms an integral part of software development, ensuring the reliability and correctness of code. Cypress (an alternative to the React Testing Library), a popular front-end testing framework, offers powerful capabilities to streamline the testing process. One critical aspect of unit testing is the ability to simulate and control dependencies, such as API calls or external libraries. This article will introduce you to stubbing and mocking in Cypress unit tests, explore their significance, and show how to effectively utilize them.

Mocking in Cypress unit tests

Mocking involves simulating the behavior of external dependencies in a controlled manner, allowing us to isolate the code under test and focus on specific scenarios. By substituting the actual implementation of a dependency with a mock object, we gain control over the data and responses returned, making our tests more predictable and reliable. Mocking plays a crucial role in unit tests by eliminating the need for real network requests and external services, thus improving test performance and reducing flakiness.

How to use mocking in Cypress unit tests

In Cypress, mocking can be achieved using the cy.intercept() method introduced in Cypress 7.0, which intercepts and controls network requests made by the application. By defining routes and their corresponding responses, we can simulate various scenarios. For example, we can mock a successful API response, simulate network errors, or delay responses to test loading states. It allows us to intercept specific URLs or match requests based on various criteria, empowering us to create precise mocks for different test cases. cy.intercept comes with different syntax. Below are various ways to write or call cy.intercept.


cy.intercept(url)
cy.intercept(method, url)
cy.intercept(method, url, staticResponse)
cy.intercept(routeMatcher, staticResponse)
cy.intercept(method, url, routeHandler)
cy.intercept(url, routeMatcher, routeHandler)

To understand the cy.intercept syntax, check here. In this article, we will use the cy.intercept(routeMatcher, staticResponse) syntax.

To demonstrate the use of mocking, consider an example where a React component relies on an API request to retrieve user data. In our Cypress test, we can mock the API response using cy.intercept(), allowing us to control the data returned and verify how the application handles different scenarios. Here’s a code example:


// defines the URL path to listen to and allows us to modify its behavior.
cy.intercept(
 {
   method: "GET",
   url: "/api/users",
 },
 {
   statusCode: 200,
   fixture: "users.json",
 }, //points to a JSON file where the response data is located
).as("getUserData");

// visits our website
cy.visit("/dashboard");

// waits and listens when the getUserData URL is called
cy.wait("@getUserData");

// runs after the getUserData URL has been called.
cy.get('[data-testid="results"]')
 .should("contain", "Book 1")
 .and("contain", "Book 2");

The cy.intercept method in the code above defines the API call Cypress should listen to and allows us to modify its behavior. It takes two arguments: the routeMatcher, and the staticResponse. The routeMatcher is where we define the request information. While the staticResponse is where we define the expected response. The fixture key in the staticResponse points to the file where our expected response data is located. The location of the data must be in the /cypress/fixtures/ directory, provided when we activate Cypress in our project. The response data is not limited to the fixtures key. Fixtures is used when the response data is in a json file. We use the’ body’ key to add our response data inside the cy.intercept method. The code looks like this


cy.intercept(
 {
   method: "GET",
   url: "/api/users",
 },
 {
   statusCode: 200,
   body: { topic: "mastering", name: "cypress" },
 },
).as("getUserData");

The.as creates a tag for that interception. As a result, we can retrieve any API call made in the test that matches the supplied arguments whenever one is there.

The cy.visit method visits our website that we are about to test and mock. cy.wait('@getUserData') tells Cypress to not perform any testing until the API call has occurred. We can also assert our API response using the .then method. Let’s write some code on how to assert the API.


// defines the path to listen to and also allows us to modify its behavior.
cy.intercept(
 {
   method: "GET",
   url: "/api/users",
 },
 {
   statusCode: 200,
   fixture: "users.json",
 }, //points to a JSON file where the response data is located
).as("getUserData");

// visits our website
cy.visit("/dashboard");

// waits and listen when the getUserData URL is being called, then asserts its response
cy.wait("@getUserData").then((interception) => {
 // Assertions based on the mocked data
});

// runs after the getUserData URL has been called.
cy.get('[data-testid="results"]')
 .should("contain", "Book 1")
 .and("contain", "Book 2");

To understand more on how to assert the API response, check here

By mocking the API response, we ensure consistent data for our test and verify the application’s behavior under different conditions.

Session Replay for Developers

Uncover frustrations, understand bugs and fix slowdowns like never before with OpenReplay — an open-source session replay tool for developers. Self-host it in minutes, and have complete control over your customer data. Check our GitHub repo and join the thousands of developers in our community.

Stubbing in Cypress unit tests

Stubbing involves replacing a function or method with a controlled implementation to control its behavior in a unit test. With this technique, we may test error handling by simulating specific circumstances, manipulating return values, or even creating forced errors. Stubbing is very helpful for managing complicated or external dependencies that are difficult to regulate or predict. In Cypress, stubbing can be accomplished using the cy.stub() method, which creates a stubbed version of a function or method. With stubs, we can define the desired behavior, return values, or even trigger specific actions when the stubbed function is called. This empowers us to create controlled test scenarios, ensuring that the code under test behaves as expected. cy.stub comes with different syntax on how to use it. Below are various ways to write or call cy.stub.


cy.stub()
cy.stub(object, method)
cy.stub(object, method, replacerFn)

For the examples in this article, we will use the second and third syntax, cy.stub(object, method) and cy.stub(object, method, replacerFn).

Suppose our application uses a utility function that generates a random number. In our Cypress test, we can stub this function to always return a specific value, allowing us to test specific edge cases consistently. Here’s a code example:


const randomStub = cy.stub(Math, 'random').returns(0.5);

cy.visit('/dashboard');

// Assertions based on the stubbed

In this example, we stub the Math.random() function to always return 0.5, enabling us to test a specific condition with a predictable outcome. Because cy.stub() creates stubs in a sandbox, all newly formed stubs are automatically reset/restored between tests without requiring your explicit reset or restore. Let’s consider other examples.

Replacing an object method with a function

let counter = 0;

const obj = {
 isStubbing(ready) {
   if (ready) {
     return true;
   }
   return false;
 },
};

cy.stub(obj, "isStubbing", () => {
 counter += 1;
});

In the code above, we replaced a method of an object with a function. Instead of the function returning true or false, it is incrementing the counter variable.

If you don’t want cy.stub() calls to appear in the Command Log, you can chain a.log(bool) method. This could be helpful if your stubs are called too frequently. Below is how to write the code.


const obj = {
 func() {},
};
const stub = cy.stub(obj, "func").log(false);

Stubs can be aliased, just like .as() does. This can help you recognize your stubs in error messages and the Cypress command log, enabling you to later assert against them using cy.get ().


const obj = {
 func() {},
}
const stub = cy.stub(obj, 'func').as('anyArgs')
const withFunc = stub.withArgs('func').as('withFunc')

obj.func()

expect(stub).to.be.called
cy.get('@withFunc').should('be.called') // purposefully failing assertion

Best practices for stubbing and mocking in Cypress unit tests

Let’s see some general considerations on testing.

Advice for effective stubbing

  • Identify the critical functions or methods to stub: Focus on functions or methods that significantly impact the code under test or interact with external dependencies.
  • Strike a balance between realism and simplicity: Aim to create stubs that mimic the real behavior of the functions or methods while keeping them simple enough for easy maintenance and readability.
  • Maintain stubs as the codebase evolves: As the application code changes, ensure that your stubs accurately reflect the updated behavior, avoiding stale or inconsistent stubs.

Advice for effective mocking

  • Understand the boundaries of mocking in unit tests: While mocking can be powerful, it’s essential to focus on mocking only the necessary dependencies and avoid excessive mocking, which can lead to brittle tests.
  • Focus on key dependencies: Mock the dependencies that significantly impact the code under tests, such as API calls or database interactions, while relying on real implementations for less critical dependencies.
  • Document and organize mock setups: Maintain clear documentation and organization of your mock setups to improve test maintainability and make it easier for other developers to understand the test scenarios.

Conclusion

For Cypress unit tests to be successful and ensure solid code quality, learning the techniques of mocking and stubbing is essential. Developers may better control their test environments, mimic different scenarios, and increase the dependability of their apps by understanding the ideas and recommended practices covered in this article. These methods will let developers create thorough and dependable unit tests using Cypress, ultimately resulting in more dependable and stable software solutions.

Gain Debugging Superpowers

Unleash the power of session replay to reproduce bugs and track user frustrations. Get complete visibility into your frontend with OpenReplay, the most advanced open-source session replay tool for developers.

OpenReplay