solving cypress component intercept puzzles
Tjerk Valentijn
Posted on September 19, 2023
At the moment I'm doing a lot of Cypress Component (cy-comp) testing for my React project. This has many advantages but also poses some challenges.
This article sums my experience with interceptors and the different problems and solutions that I ran into.
When running cy-comp tests you usually have a React (Container) component that does HTTP but your test does not have access to the URL.
Intercept to the rescue
Luckily the vast Cypress toolbox contains a powerful weapon to help with this challenge. intercept provides a declarative way to stub network requests. This is a good idea because it also speeds up your tests a bit.
Example
In the following example I test the interaction between a React Container Component and the back-end service it uses. A user is needed to render a form.
// imports left out for brevity
const BASE_URL = "/api/company";
const CID = "94bb3e36-8c1c-44ee-bd84-c329a0a3a6cb";
const UID = "7d8f3259-5928-40af-9633-e2ece7ed55ef";
describe("when both HTTP requests succeed", () => {
it("should show the form", () => {
cy.intercept(
{
method: "GET",
url: `${BASE_URL}/${CID}/user/*`,
},
{
firstName: "Cassian",
id: UID,
},
).as("get-user-happy-flow");
// attach Container to the DOM
mount(<UserContainer companyId={CID} userId={UID} />);
// assert the form is loaded
cy.get("#user-form").should("contain", "user.view.pageHeader");
});
});
Problem 1: The 'no overwrite' problem
Now you add a test where one of the calls fails with an HTTP 500 server error.
cy.intercept("GET", `${BASE_URL}/${CID}/user/*`, (req) => {
req.reply({
statusCode: 500,
body: {
error: "The Server is Dead",
},
});
}).as("get-user-500");
The latest interceptor for the URL should take precedence, but
You run into the problem that the oldest interceptor is not overwritten. This is sometimes related to timing issues where the right interceptor is not loaded at the right time. The solution is then to 'wait' for the proper interceptor to have been fired.
cy.wait("@get-user-500")
.its("response.statusCode")
.should("be.oneOf", [500]);
Problem 2: wait could not be the solution to the problem
Sometimes wait is is not the solution. Then the interceptor for the second test is not loaded despite of the wait. Something else is wrong but you do not succeed in discovering why.
Then you can make more specific URLs. The idea is that instead of re-using the company ID (CID) you call for two distinct companies. That way the interceptors do not need to overwrite each other.
cy.intercept(
{
method: "GET",
url: `${BASE_URL}/some-other-cid/user/*`,
},
{
id: CID,
name: "Sith Lord Inc.",
},
).as("get-company-500");
Problem 3: it still does not work
Sometimes it is not your day and the right interceptor is still not loaded. As a busy developer you need to push something that works today.
A catch-all solution is to do some dependency injection instead of interception. Note that you need to change your production code to enable this feature. Some people argue not to change your code to improve test-ability, but that is a discussion for another time.
In React you can do simple dependency injection by passing a collaborating function or object into a View Component as a prop. In this example I can inject the function that handles the HTTP calls.
// imports left out for brevity
interface Props {
companyId: ID;
userId: ID;
getFn?: typeof useGetUserQuery;
}
const UserContainer = ({ companyId, userId, getFn = useGetUserQuery }: Props) => {
const queryResult = getFn(companyId, userId);
return queryResult.isSuccess ? (
<UserForm user={queryResult.data} />
) : (
<div>{"error"}</div>
);
};
The non-test/production context will not provide a getFn and allows the Container to fallback to the default which is the real useGetUserQuery (redux-toolkit-query hook).
The test context overrides the default getFn with a stub that has the same shape as the real useGetUserQuery.
it("should NOT render the provided content if there was an HTTP 500", () => {
const getFn = () => ({
isSuccess: false,
status: 500,
error: "The Server Died",
});
mount(
<UserContainer
companyId={"irrelevant-id"}
userId={"irrelevant-id"}
getFn={getFn}
></UserForm>,
);
cy.get("body").should("contain", "error");
// other assertions left out for brevity
});
conclusion
Cypress Components is nice for developer experience and tooling. However, it can have some quirks as I've found out the last year or so.
Hopefully, when you run into problems with interceptors, you can use the strategies I've mentioned in this article to fix them.
If you have other or better fixes for these kinds of problems, please let me know.
Posted on September 19, 2023
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.