My Software Engineering Notes Help

React Testing Crash Course

reactjs

Abbreviations

  • *[E2E]: End-to-End

Introduction

Learn about testing in React including unit, e2e and integration testing with React Testing Library, Jest & Cypress

Why Should You Test?

  • Goal: check whether an application behaves as expected

  • Safeguard against unwanted behavior when changes are made

  • Automated, and thus efficient in the long-term

What to Test

Have a test priority (example):

  1. High-value features— E2E cypress

  2. Edge cases in high-value features - Integration / Unit Testing-Library

  3. Things that are easy to break - Integration / Unit Testing-Library

  4. Basic React component testing - Integration / Unit Testing-Library

    • User interaction - Integration / Unit Testing-Library

    • Conditional rendering - Integration / Unit Testing-Library

    • Utils/Hooks - Integration / Unit Testing-Library

Demo App Setup

  1. Download zip file or clone this repo

  2. Unzip, if you downloaded the zip file.

  3. cd into project folder

  4. run yarn install && yarn dev

  1. login in with username: 'johndoe' and password: 's3cret'

Unit Tests

Test a very small part of the code, usually a function.

  1. create a new test file for whichever component you want to test. (i.e., component name is TransactionCreateStepTwo.js then test file name would be TransactionCreateStepTwo.test.js) (example test file: transactionCreateStepTwo.test.js

    1. render the component you want to test

      1. screen.debug() - debug() - shortcut for console.log(prettyDOM(baseElement)) - if the baseElement is not specified, it defaults to document.body

      2. screen.getByRole("button", { name: /pay/i })).toBeDisabled(); - getByRole - react-testing-library query to see what roles are available

      3. now create your assertion statement expect(await screen.findByRole("button", { name: /pay/i })).toBeEnabled(); which should fail now

      4. now create your assertion statement expect(await screen.findByRole("button", { name: /pay/i })).toBeDisabled(); which should pass now

      5. userEvent.type(screen.getByPlaceholderText(/amount/i), "50"); - mimics a user typing an amount in the textbox for amount - on this example have to use getByPlaceholderText because on this form there is no label or name associated with it.

      6. userEvent.type(screen.getByPlaceholderText(/add a note/i), "dinner"); - mimics a user typing a note in the textbox for note - on this example have to use getByPlaceholderText because on this form there is no label or name associated with it.

Here we tested user interaction and conditional rendering by first seeing if the 'pay' button was disabled on initial render, added user interaction by typing in values to the two inputs and then testing to make sure the 'pay' button became enabled.

Integration Tests

Testing whether multiple units within the app are working correctly together. These types of tests usually have multiple assertions in them. There is not a consensus on what makes a unit test vs. an integration test.

When writing an integration test, it is good to think about actual user flows. Example: user clicks on '$ NEW', searches for another user, clicks on the appropriate user and then fills in amount, adds a note and then clicks on 'PAY' button.

A good example of a simple integration test then would be to combine the two unit tests written in the last section by moving the assertion from the first test into the second test before the user interaction and then deleting the first test.

test("amount and note entered, pay button becomes enabled", async () => { render(<TransactionCreateStepTwo sender={{ id: "5" }} receiver={{ id: "5" }} />); expect(await screen.findByRole("button", { name: /pay/i })).toBeDisabled(); userEvent.type(screen.getByPlaceholderText(/Amount/i), "50"); userEvent.type(screen.getByPlaceholderText(/Add a note/i), "dinner"); expect(await screen.findByRole("button", { name: /pay/i })).toBeEnabled(); });

This does not mean this is how integration tests are to be written, but if combining more than one unit test represents how a realistic user flows, then by all means do this. In this example, it just worked out that way.

Often, a few integration tests are better than numerous, small and concise unit tests.

End-to-End Tests

Tests all the way from the back end to the front end. In essence, mimicking how a user would interact and use the application in the browser.

For this, a testing library known as Cypress will be used. This does not come with CreateReactApp by default, so there is some setup involved.

  1. install Cypress

yarn all -D cypress @testing-library/cypress
  1. open cypress

yarn run cypress open
  1. remove Cypress default tests by navigating to '/cypress/integration' and remove any folders in there.

  2. (optional) add react-testing-library cypress commands—this allows you to use the commands from the previous tests in out cypress tests

    1. go to '/cypress/support/commands.js'

    2. add the following

    import '@testing-library/cypress/';
    1. close all open files

  3. (optional) if not already installed, then add Testing Playground Chrome extension - allows you to hover over elements of your page and suggests queries [//]: # (TODO: fix link to payments.spec.js)

  4. go to '/cypress/integration' folder and add [[javascript.react-js.testing.react-testing-crash-course.examples.payment_specjs]]

    1. in the cypress window, click on 'Run 1 integration spec' to see the results of the test

line 37 in paymentSpec Example cy.findByText(note).click({ force: true }); forces cypress to click regardless. If this is the .click({ force: true }) is not included, cypress may error out since it takes the page some time to fully load and the element it is supposed to click maybe hidden from view by the navbar.

lines 40 & 41 in paymentSpec Example

cy.findByText(`-$${paymentAmount}`).should("be.visible"); cy.findByText(note).should("be.visible");

are cypress version of assertion statements

line 47 in paymentSpec Example expect(convertedOldBalance - convertedNewBalance).to.equal(parseFloat(paymentAmount)); is a custom assertion

Last modified: 10 March 2024