Sign in
Log inSign up
Let's write e2e tests for a React application with Cypress

Let's write e2e tests for a React application with Cypress

James George's photo
James George
·May 5, 2020

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, 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.

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.

yarn add -D cypress

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

./node_modules/.bin/cypress open

Alternatively use the shortcut npm bin

$(npm bin)/cypress open

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

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:

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

A final version would look like the one below:

// cypress.json
{
  "baseUrl": "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.

yarn add -D start-server-and-test

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

"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 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.
  2. 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.
  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'.
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 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 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() function as provided by Cypress for the purpose.
  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.
  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.
  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().
  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.
  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.
  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.
  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. Thanks for reading through.

If you wish to catch up with my work, follow me on twitter.