Let's write e2e tests for a React application with Cypress
Tags: testing, webdev, showdev, javascript
Published: 2020-05-05 12:25:17

End to end testing is one of the testing methodologies which is supposed to check whether if an application works as expected or not, by testing the so called user flow.

Well let's write couple of e2e tests for one of the React applications that I've made: [csstox](https://csstox.surge.sh/), a simple utility with which you can convert CSS snippets to React Native/JSS stylesheet objects with ease. Please read through the behind story [here](https://dev.to/jamesgeorge007/csstox-convert-css-snippets-to-react-native-jss-stylesheet-objects-with-ease-2500).

## Getting started

As evident from the title we would be making use of Cypress as the testing framework. First up we need to have cypress installed as a devDependency. 

```bash
yarn add -D cypress
```

The following command creates couple of files and directories as needed by Cypress.  

```bash
./node_modules/.bin/cypress open
``` 

Alternatively use the shortcut `npm bin`

```bash
$(npm bin)/cypress open
```

After couple of tweaks the directory structure for the test setup looks like the one below:

```bash
tests
└── e2e
    ├── integration
    │   ├── basic.spec.js
    │   └── behavior.spec.js
    └── screenshots  
```

Next up we need to configure Cypress based on the changes made, and we've got `cypress.json` file for this purpose.

Let's make Cypress aware that it has to search for the intended files within `tests/e2e/integration` directory:

```bash
"integrationFolder": "tests/e2e/integration",
```

A final version would look like the one below:

```json
// cypress.json
{
  "baseUrl": "http://localhost:3000",
  "integrationFolder": "tests/e2e/integration",
  "screenshotsFolder": "tests/e2e/screenshots",
  "supportFile": false,
  "pluginsFile": false,
  "video": false
}
```

Also, Cypress would require our application to be up and running prior to start executing the tests. Let's install a utility that would do this for us.

```bash
yarn add -D start-server-and-test
```

Let's go ahead and add the following scripts to `package.json`

```bash
"cy:run": "cypress open",
"test:e2e": "start-server-and-test :3000 cy:run"
```

With that we can launch the test setup with `yarn run test:e2e`.

[start-server-and-test](https://github.com/bahmutov/start-server-and-test) by default looks for a start script, luckily this is the case for us. Or else we're required to provide the associated script name as the very first argument followed by the local server URL and test script. And we're all set to start writing tests for our app.

As you might have noticed from the directory structure above, there are two test suites:
1. Basic Workflow - This one's kinda like smoke-tests, it ensures that things are ready to carry out further test cases.
- Behavior - It includes test cases that ensures the end to end behavior of the application.

### Basic workflow

- First up we need to ensure if our app is up and running.

```js
  it("renders without crashing", () => {
    cy.visit("/");
  });
```

- We've a select box as part of the UI which defaults to the value - 'React Native'. Cypress provides various commands to interact with the DOM as a real user would do. Here, we need a utility that would pick up the select box and makes sure that it defaults to the value, 'React Native'.

```js
it("expects to find the select box defaulting to React Native", () => {
    cy.visit("/")
      .get("[data-testid=selectbox]")
      .should("have.value", "React Native");
  });
```

As you might have noticed an attribute (`data-testid`) selector is used instead of a class selector, you might be wondering why. There're couple of best practices listed in the Cypress docs website and you can find [selecting elements](https://docs.cypress.io/guides/references/best-practices.html#Selecting-Elements) to be one among them. CSS classes are subjected to change anytime causing the test case to fail which would not be the case with `data` attributes. As expected we've refactored the [respective component](https://github.com/jamesgeorge007/csstox/blob/e1916115adff82f3ee980fcb4b46b6477f6e3b05/src/components/select-box/SelectBox.js#L12) to have a `data-testid` attribute.

Cypress comes up with a handful of assertions to choose from which are made available from assertion libraries such as `chai`, `sinon` etc. One can create an assertion with `should()`, and now we've have a better picture.

### Behavior

Hurray, we've just finished writing test cases for the first test suite. And now we're off to write tests that describe the behavior of the app in detail.

- We've two `textarea` elements which serves different purpose. The one on the left is supposed to allow the user paste a CSS snippet while the other one should be displaying the React Native/JSS equivalent of the same. This calls for the need to type some input CSS snippet to the respective `textarea` element. Luckily we've a `type()` command as provided by Cypress for the purpose.

```js
  it("is possible to enter text to the `textarea` intended to receive input CSS snippet", () => {
    const cssSnippet = "padding: 10px;";
    cy.visit("/")
      .get("[data-testid=input]")
      .type(cssSnippet)
      .should("have.value", cssSnippet);
  });
```

- As said before both the `textarea` elements perform different roles. The one on the right is supposed to display the React Native/JSS equivalent which should be made not editable by the user. How're we gonna write a test case for this scenario? Well, it's pretty simple. Just make sure that the respective `textarea` element has got a `readonly` property.

```js
  it("expects to find readonly attribute associated with the textarea intended to display the result", () => {
    cy.visit("/").get("[data-testid=output]").should("have.attr", "readonly");
  });
```

- And now we need to write a test case to ensure if the application serves it's purpose, i,e if an input CSS snippet is being converted to the respective equivalent.

```js
  it("converts an input CSS snippet to the React Native equivalent", () => {
    const inputCSSRule = "transform: translate(10px, 5px) scale(5);";
    const result = {
      transform: [{ scale: 5 }, { translateY: 5 }, { translateX: 10 }],
    };
    cy.visit("/")
      .get("[data-testid=input]")
      .type(inputCSSRule)
      .get("[data-testid=output]")
      .should("have.value", JSON.stringify(result, null, 2));
  });
```

- Here comes the JSS counterpart presenting before us a new challenge. The select box defaults to the value - 'React Native', we're required to change the value to JSS and Cypress comes to the rescue with `select()`.

```js
  it("converts an input CSS snippet to the JSS equivalent", () => {
    const inputCSSRule = "margin: 5px 7px 2px;";
    const result = {
      margin: "5px 7px 2px",
    };
    cy.visit("/")
      .get("[data-testid=selectbox]")
      .select("JSS")
      .get("[data-testid=input]")
      .type(inputCSSRule)
      .get("[data-testid=output]")
      .should("have.value", JSON.stringify(result, null, 2));
  });
```

- We've validations in place to ensure submitting an invalid CSS rule results in an appropriate warning being displayed in the output `textarea` element. Well, let's write a test case for it.

```js
  it("shows an error message for invalid CSS snippet", () => {
    const inputCSSRule = "margin: 5";
    const result = `Error translating CSS`;
    cy.visit("/")
      .get("[data-testid=input")
      .type(inputCSSRule)
      .get("[data-testid=output]")
      .should((el) => {
        expect(el).to.contain(result);
      });
  });
```

- If the input `textarea` element is left blank we've a placeholder in place and the equivalent version is displayed on the output `textarea` element.

```js
  it("generates the React Native equivalent of default CSS rule available as placeholder", () => {
    const result = {
      fontSize: 18,
      lineHeight: 24,
      color: "red",
    };
    cy.visit("/")
      .get("[data-testid=output]")
      .should((el) => {
        expect(el).to.contain.text(JSON.stringify(result, null, 2));
      });
  });
```

- And the JSS counterpart.

```js
  it("generates the JSS equivalent of default CSS rule available as placeholder", () => {
    const result = {
      fontSize: "18px",
      lineHeight: "24px",
      color: "red",
    };
    cy.visit("/")
      .get("[data-testid=selectbox]")
      .select("JSS")
      .get("[data-testid=output]")
      .should((el) => {
        expect(el).to.contain.text(JSON.stringify(result, null, 2));
      });
  });
```

And that's pretty much it. We have gone through only a few things that Cypress offers, please get to know more from the [official docs](https://docs.cypress.io/guides/overview/why-cypress.html). Thanks for reading through.

If you wish to catch up with my work, follow me on [twitter](https://twitter.com/james_madhacks).