solving cypress component intercept puzzles
Tags: cypress, typescript, http, react
Published: 2023-09-19 13:37:41

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](https://www.patterns.dev/posts/presentational-container-pattern) 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](https://docs.cypress.io/api/commands/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.

```typescript
// 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.

```typescript
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**.

```typescript
 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.

```typescript
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. 

```typescript
// 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](https://redux-toolkit.js.org/rtk-query/overview) hook).

The test context overrides the default **getFn** with a stub that has the same shape as the real useGetUserQuery.

```typescript
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.