solving cypress component intercept puzzles

tjercus

Tjerk Valentijn

Posted on September 19, 2023

solving cypress component intercept puzzles

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");
  });
});

Enter fullscreen mode Exit fullscreen mode

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");
Enter fullscreen mode Exit fullscreen mode

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]);
Enter fullscreen mode Exit fullscreen mode

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");
Enter fullscreen mode Exit fullscreen mode

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>
  );
};
Enter fullscreen mode Exit fullscreen mode

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
});
Enter fullscreen mode Exit fullscreen mode

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.

💖 💪 🙅 🚩
tjercus
Tjerk Valentijn

Posted on September 19, 2023

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related