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 asAPI
calls or external libraries. This article will introduce you to stubbing and mocking inCypress
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.