Back

Implementing Your First End-to-End Tests In React Using Cypress

Implementing Your First End-to-End Tests In React Using Cypress

Introduction

Cypress is the leading end-to-end (‘E2E’) tool for frontend testing, but many frontend developers have only a passing familiarity with testing as a concept. If you’re confused or uncomfortable with testing in a frontend environment, and perhaps associate it purely with unit/component testing or with the backend, then you’ve come to the right place. In this article, we’ll delve into why E2E tests are important; how using Cypress can save you time; and of course learn how to implement the basics of Cypress into a ‘MadLibs’ ReactJS application.

1. Testing and the frontend

Test-driven development (‘TDD’) is in the mouths of every backend developer, as testing has finally assumed its rightful position as a ‘must-have’ of software engineering. Any company not utilising backend testing is behind the curve. However, this surge in interest has only recently begun picking up steam for the frontend. Anyone transitioning to frontend development is likely to feel very comfortable with unit testing, where specific components are checked to ensure they’re producing the right outputs. However, frontend E2E testing is an entirely different beast.

So how do you end-to-end test a front-end app? Enter Cypress.

The key concept to understand in frontend testing is that we want to test results, not implementation details. In other words: does the screen end up looking how we want it to look, and doing what we want it to do? Great! We don’t care about specific function calls, state, or data structures here. We’re simply testing the user experience.

2. Arrange/Act/Assert

Arrange/Act/Assert is a simple way to approach testing, which can help you understand the basic process by which all tests (including backend!) are structured. They also form a good basis for explaining the basics as we move through our demo application. In short, Arrange/Act/Assert is a framework for structuring your tests.

The first step when writing a test is to ‘arrange’ the right environment. In our case, you’ll see we need to load up the page we want to test.

Next, we ‘act’ by doing an action (or series of actions) necessary to replicate how the user could interact with our app. Another way of thinking about it is that the ‘Act’ is the action we’re testing.

But of course, there’s no point loading a page, entering some data, and clicking “Submit” without actually telling our tests what we expect. Do we want Submit to result in a popup or a page load? This is where ‘assert’ comes in: we tell our test file exactly what outcome we expect, based on the actions in the previous steps.

3. Introducing our example app

For the sake of this demo, I’ve created a small ‘MadLibs’ app with React. For those unfamiliar with MadLibs as a concept, it involves writing a short story with some gaps in it, filled with words entered by the user. The catch? The user doesn’t know how the words will be used! They enter the words according to what is needed (‘animal’, ‘food’, etc.) and then the author assembles them in a way which can produce some really weird and wonderful stories.

In our app, we ask for ten entries from our user. Once they’re all entered, they can submit the responses and up pops a little story complete with their own words. That’s it. That’s the whole app.

With this simple example, we can demonstrate some basic functionalities of Cypress and create a jumping-off point for your exploration of frontend E2E testing.

Remember, we don’t care what component is loaded, what function is being called, what state is being used, or anything to do with code. All we’re testing is what the user can (or cannot) do.

Getting Cypress working

Once you have the app cloned locally, cd into it, then run npm install and npm start. This should get the React App started, and you can have a go at my little MadLibs story! Fun!

But of course, we’re not just here to mess around with my expert writing talents. We’re here to learn Cypress! Head back to the Terminal and enter the following from inside the project directory.

npm install --save-dev cypress
npx cypress open

This second command is how you’ll continue to access cypress, although you can also consider adding it as an addition npm script in your package.json.

 "scripts": {
    "start": "react-scripts start",
    "build": "react-scripts build",
    "test": "react-scripts test",
    "eject": "react-scripts eject",
    "e2e-test": "cypress open"
  },

Now, if I want to run cypress, I can use npm run e2e-test in the Terminal. In the end, though, it’s up to you whether you take this extra step and if so, how you want to name your script.

Let’s run this command and see what happens…

Firstly, a new cypress window will open, presenting you with some pre-loaded tests you can use as a reference.

Screenshot 2021-09-12 at 15.31.16.png

Cypress has also taken the liberty of creating a folder called cypress within your project, which includes plenty of examples for you to refer to. We won’t need these examples today, so it’s up to you if you keep them for your own reference or delete them.

Does our page load in Cypress?

The first step to testing in Cypress is loading up your application. Luckily this is pretty simple! Firstly, we need to create a new test folder.

mkdir ./cypress/integration/0-my-madlibs

In this folder, create a new file with the ending .spec.js: this signals to Cypress that it will be a file running tests, and it will automatically appear in the Cypress GUI.

Screenshot 2021-09-12 at 15.37.17.png

Our test file is currently just sitting there doing nothing, so let’s connect to our app. Firstly, we need to start the app (if it’s not still running from earlier) using npm start. Once it’s loaded, we should see our Super Cool MadLibs(TM) in the browser at http://localhost:3000.

Now we have to update our file to include our page load:

describe('The MadLibs Main Form', () => {
    it('loads successfully', () => {
        cy.visit('http://localhost:3000')
    })
})

Let’s talk about what is happening here:

describe is used to group tests which fit together, and it takes two arguments:

  1. a string where you literally describe what is being tested (e.g. component/page)
  2. a callback function which should include your test setup and assertions

it is what we use to indicate our individual tests, and it again takes two arguments:

  1. a string which describes what the test should achieve
  2. a callback function with an individual test’s steps

cy.visit is a cypress command that tells the browser to go to the nominated address

Right now, this isn’t actually testing anything. We are still in the ‘arranging’ phase at this point. Nevertheless, it’s important to know that this is working. Go to the Cypress GUI and click on the test. It should open a new browser window and display the page.

Screenshot 2021-09-12 at 15.55.34.png

Note that the test will officially ‘pass’, even though we haven’t asserted anything, because nothing has gone wrong. In other words: Cypress tests always pass until they fail, not the other way around.

To make sure our form is actually loading, we may want to check that the navbar, header, table, and button are all there.

describe('The MadLibs Main Form', () => {
    it('loads successfully', () => {
        // ARRANGE
        cy.visit('http://localhost:3000')

        // ACT
        // None: Loading only

        // ASSERT
        // Navbar
        cy.get('nav')
            .should('be.visible')
            .within(() => {
                cy.get('h1')
                    .should('contain.text','My Cool MadLibs')
                cy.get('a')
                    .should('be.visible')
                    .should('contain.text', 'Exit Site')
            })

        // Form
        cy.get('h2').should('contain.text','Enter Your Choices!')
        cy.get('table').should('be.visible')
        cy.get('tr').should('have.length', 10)
        cy.get('button')
            .should('contain.text','Complete')
            .should('be.disabled')
    })
})

Cypress uses jQuery under the hood, but even if you haven’t used jQuery before, the .get method should be pretty easy to understand, as it uses the same syntax as CSS selectors. Put another way, it uses the a combination of JavaScripts .querySelector and .querySelectorAll: it queries based on the selector and if one element is returned, then only that element is acted upon. If multiple elements are returned, then a collection is acted upon.

Once you .get the HTML element you’re looking for, you can then chain multiple assertions. You might also notice that once we find a unique element, we can also limit our assertions to within that element using .within and a callback function, as we do above for our navbar.

Here, we’ve made the test longer and more detailed than it really needs to be, in order to demonstrate a range of basic assertions available to us:

  • .should('be.visible'): The element is visible to the user.
  • .should('contain.text','My Cool MadLibs'): The element contains the text entered as the second argument.
  • .should('have.length', 10): The number of elements returned should be 10.
  • .should('be.disabled'): This element should be disabled (i.e. not clickable).

Of course, there are a myriad of other assertions available from which we can call.

Open Source Session Replay

OpenReplay is an open-source, session replay suite that lets you see what users do on your web app, helping you troubleshoot issues faster. OpenReplay is self-hosted for full control over your data.

replayer.png

Start enjoying your debugging experience - start using OpenReplay for free.

Adding some actions

Now that we know how to Arrange (e.g. load the page) and Assert (e.g. check things are as we want), let’s add arguably the coolest part of Cypress: ACTING!

200.gif

We’re going to have Cypress fill out our form and make sure our story is exactly how we want it to be. Let’s do some automated MadLibs!

Firstly, does our button activate when all our fields are filled in? It’s important to separate out this test, because if it fails in future we can immediately identify that there’s an issue with our button, and not with our results.

it('activates the button when the form is filled in', () => {
        //ACT
        cy.get('input#animal').type('platypus')
        cy.get('input#action').type('caressing')
        cy.get('input#object').type('vacuum cleaner')
        cy.get('input#food').type('popcorn')
        cy.get('input#name').type('FLANJESUA THE ADORABLE')
        cy.get('input#drink').type('chocolate milk')
        cy.get('input#number').type('8')
        cy.get('input#adjective').type('flowery')
        cy.get('input#city').type('Copenhagen')
        cy.get('input#mood').type('inconsolable')

        //ASSERT
        cy.get('button').should('be.enabled')

    })

Saving this (or your own choice of entries) and observing the Cypress runner executing the test, you will see the form being filled out! COOL!

Since most forms will include other fields as well, here are some other form actions you can take:

  • .check() or .uncheck() for checkboxes
  • .select('Germany') for select elements. For example, this would select the ‘Germany’ option.
  • .trigger('change') to trigger an event on a DOM element. For example, this could be the first step to triggering a value change on a slider input.

Now let’s do the fun bit and click our button! More theatre, please!

it('shows the completed story with our input data',() => {

    //ARRANGE
    const finalStory = `Once upon a time in Copenhagen, FLANJESUA THE ADORABLE got out of bed to start their day. To their surprise, sitting at the end of their bed was an enormous platypus caressing the vacuum cleaner.FLANJESUA THE ADORABLE felt so inconsolable, they knocked over the glass of chocolate milk on their bedside table.Suddenly, the platypus spoke..."You must answer 8 flowery questions, or I will steal your soul... and your popcorn!"`

    //ACT
    cy.get('button').click()

    //ASSERT
    cy.get('div.results').should('contain.text', finalStory).within(() => {
        cy.get('h2').should('contain.text', 'Results')
    })

})

Importantly, the other two tests here have also been part of the ‘Arrange’ step for us. We’ve set up the environment we need to act and assert for our final story. By keeping these in the same test file, we don’t need to rearrange the loading of the page, for example.

Here, we “Act” by running click() on our button. After that, we “Assert” by checking that our final story (here saved to a constant) is displaying properly, and then just for kicks we ensure our header has changed as well.

Let’s see what Cypress has to say about this…

Screenshot 2021-09-15 at 17.45.18.png

Wonderful! All tests pass!

Of course, clicking isn’t the only mouse event we can trigger, here are some others:

  • .dblclick()
  • .hover()
  • .rightclick()

There are a huge variety of commands available, of course. For a full list, refer to the documentation here.

Thank you! The basics make sense now! What’s next?

The beautiful part of Cypress is, of course, watching a huge suite of tests execute together. If we ‘break’ our app, in this example by changing our story, messing up our button disabled state, or accidentally importing too many fields, our tests will fail and we can know immediately that we’ve messed something up. By using descriptive naming of our tests and separating them out into distinct steps, it should also be clear what we’ve messed up.

If you want to expand your use of Cypress, then of course the docs are a good place to start, and I would also recommend looking into additional commands which can assist in a DRY approach, for example using .each() to loop through assertions. Most of Cypress can be learned through applying it to an existing frontend application and simply testing the bounds of how and upon what you can act and assert.

Used appropriately, Cypress can form the basis of a TDD approach to frontend development. Imagine a world in which we decide what we want our new feature to do and how it will look, describe it in Cypress in distinct steps, and then watch more and more tests pass as we build it out. Such an approach could help us plan our features more carefully, and keep us goal-focused when building it out.

Cypress is not just a super cool thing to watch execute (which it DEFINITELY is), but also an essential tool for streamlining your testing, picking up problems early, and saving the time and money of manual testers clicking through your application. Get started today by covering the basics, and maybe you’ll see more information on digging deeper into Cypress testing soon!