Skip to content

Latest commit

 

History

History
381 lines (257 loc) · 14.9 KB

frontend-testing.md

File metadata and controls

381 lines (257 loc) · 14.9 KB

Frontend Testing

Table of content

Tools and setup for testing

This section offers an overview of our most used tools.

Quick links

  • Jest
  • react-testing-library
  • jest-dom-matcher
  • user-events

Jest

🔗 Jest - official documentation

We use Jest as test runner in Frontend in general and use the framework in our TypeScript tests. Jest is a testing framework with integrated test-runner for JavaScript. It can be used with Vanilla JS, ES6, TypeScript, Vue, React and so on. .

react-testing-library

🔗 react-testing-library - official documentation

⚠️ We use and need this library only for testing React components.

Dom testing library is an opinionated library, which supports a specific way of writing test. It is well documented and supports us in having a consistent code base. It’s best used with jest-dom and the custom jest-dom matchers. The combination of both leads to nice readable tests!

react-testing-library is a react-adapter for the DOM testing-library.

jest-dom matchers

🔗 jest-dom matchers - official documentation

To enable us to test the DOM, we use the standard JSDOM of Jest as environment. Additionally, we use the @testing-library/jest-dom library, which provides a set of custom jest matchers (e.g. .toBeEnabled() or .toBeChecked()) to extend the jest matcher. You should use them for your assertions. These will make your tests more declarative, clear to read and to maintain.

user-events

🔗 user-events - official documentation

user-event is a higher-level abstraction of fireEvent from DOM testing library and tries to simulate the real events that would happen in the browser as the user interacts with our components.

Frontend testing approach

General rules

  • We test all code.
  • Test files live right beside the code they test.
  • Extract functionality in pure TypeScript files where possible. They are faster and less complicated to test than React components.

Goals of testing approach

Our tests...

  • 🧑 confirm code works as expected for users.
    • User is the consumer of the web app.
    • User is a developer in the code base.
  • ✅ confirm behavior and functionality for their component.
    • Test every component explicitly.
  • 🔎 help us to locate an error fast.
  • 📕add readability to our code base.
    • A dev can understand what a component does by looking only at the test.

How to test

How to get the best out of our libs

Use screen when possible

The screen object has every query (method used to query for elements) pre-bound to document.body. It is used like this:

import { screen } from "@testing-library/react";
// ...
expect(screen.getByRole("button", { name: "click" })).toBeEnabled();

We recommend to use screen where it is possible. It helps developer experience because you can see which queries are available on it. There are very few cases where you would need to use the container element returned from render.

A great benefit is that this can help us making our markup more accessible. If you can't use the recommended query, you should check if there is a reason for that.

Example: There is an alert, and you can't query it other than with looking for the text? That means assistive technology also has no information that this is a warning. Changing that element to have an alert role enables you to use the recommended queries. And it makes the content more accessible!

💡 Tip to remember the right query

testing-library has an experimental config. It can throw an error if there is a better query available. For example when using .getByText("click") instead of .getByRole('button', { name: "click" }).

It also suggests a better available query! (it suggests regex in name, so, it’s not perfect 😅, but still!)

You can make use of that by adding at top of your test file:

configure({
throwSuggestions: true
})

This adds the config only to your test file. No others are not affected! The config is making mistakes and still experimental, so we won't use it in a general config. It's up to you if you want to remove it in your test file after writing the tests.

⚠️ Note: it only suggests a better query if the element in the component is semantically correct / accessible. It only solves something on the test side (using better queries to make test implicitly test more important things), not on the code side.

make use of the jest-dom matcher

These matchers not only make the tests more declarative, they also support to write test in a natural feeling and user-centered way. The library is actively worked, so make sure to check out their documentation to find useful matchers for specific use-cases!

It’s a good practice to choose a precise matcher. For example .toBeEnabled() is preferred to .toBeVisible() if testing a (non-disabled) button in your React component.

Example and benefits of jest-dom matcher

(in combination with the queries)

it('shows a checkbox to confirm user has read the text', () => {
const checkbox = screen.getByRole('checkbox', { name: 'Confirm that I read the text' })

expect(checkbox).toBeEnabled()
expect(checkbox).not.toBeChecked()
})

getByRole makes sure there is an accessible (for the user but also for the machine) checkbox element with an associated and readable name to it. It will fail if someone removes type="checkbox" by accident from the input or replaces the input with a div, which could also would affect functionality.

It’s not necessary to use toBeVisible() on e.g. form elements. If they are not visible, getByRole will throw an error - they are not accessible if they are not visible. (There’s a feature to actually test hidden elements by their role, but there’s almost never a need for it). Also, toBeEnabled() is implicitly confirming that an element is visible.

.toBeChecked() is another custom matcher from jest-dom. Only a valid, semantic and at least mostly accessible html element can be asserted with this. It can't be used on a button type="button" for example. This implicitly make sure all these things are working correctly.

How to structure test files

The first describe block:

  • wraps all the tests for one file
  • contains only the name of the file we are testing (e.g. MyComponent.tsx or fancy-util.ts)

Example React component

describe("Dashboard.tsx", () => {
  // all related 'describe's, and 'it's
});

Example ts-file

describe("fancy-util.tsx", () => {
  // all related 'describe's, and 'it's
});

How to group tests meaningful

Group your tests with nested, meaningful describe scopes.

Jest offers global methods to create blocks of tests. The most used for us are describe and it (it is an alias for test).

`describe` creates a block that groups together several related tests.
`it` is the method that actually runs one test.

Grouping several it in a describe block and nesting describe blocks create a more structured test file. It also translates to a nicely readable output in the test runner.

A describe block describes a scenario or test case. An it block describes the specification of different assertions in this scenario. A rule of the thumb for unit tests is to only have one assertion (expect) in one it block. This helps us find the cause of a failing test faster. Grouping multiple it blocks in one describe makes it easier to archive this.

The nesting with describe blocks should ot be overused.

️✅ Do

describe('nameOfFunction', () => {
  it('returns true when user chose "cat"', ...)

  it('returns true when user chose "dog"', ...)
})

⛔️ Don't

describe('nameOfFunction', () => {
  describe('when user chose "cat', () => {
    it('returns true', ...)  })
  describe('when user chose "dog"', () => {
    it('return false', ...)
  })
})
Example: Grouping tests that check the default markup is rendered
describe('MyNiceComponent.tsx', () => {
  describe('renders a page with default props', () => {
    it('shows a headline', () => {
     ...
    })
    it('shows an input field', () => {
     ...
    })
    it('shows a disabled button', () => {
     ...
    })
  })
  describe('now there are more tests', () => {
  })
})

This generates the following output when running the tests:

MyNiceComponent.tsx
  shows a page with default props
    ✓ renders a headline
    ✓ renders an input field
    ✓ renders a disabled button
Example: Grouping tests that test user interaction
describe("MyNiceComponent.tsx", () => {
  //...
  describe("user can fill out the email field and send the form", () => {
    it('shows a disabled "Send" button while the input is empty', () => {
      //...
    });

    it("enables user to fill out input field", () => {
      //...
    });

    it('shows an enabled "Send" button when user filled out input', () => {
      //...
    });

    it("enables user to send the form with their input", () => {
      //...
    });
  });

  describe("now there are more tests", () => {
    //...
  });
});

The test output now looks like this:

MyNiceComponent.tsx
  shows a page with default props
    ✓ shows a headline
    ✓ shows an input field
    ✓ shows a disabled button
  user can fill out the email field and send the form
    ✓ shows a disabled "Send" button while the input is empty
    ✓ enables user to fill out input field
    ✓ shows an enabled "Send" button when user filled out input
    ✓ enables user to send the form with their input

How to write test descriptions

We should use a consistent approach and language in our test description. This help to understand and document the code they are referring to!

🔖 You can read more in the article Art of test case writing.

General tip

Use the describe block to describe a scenario or test case.

This block can:

  • describe a larger block of functionality.
  • contain a set of specific requirements for one test case.
  • group one user-path.

Examples:

  • describe "renders all necessary elements"
  • describe "when user selects a certain environment"
  • describe "handles the form for deleting a topic"

The it block contains the specification itself. Together with its assertion it describes the expected outcome. Keep in mind that one-assertion-per-it is a good rule to follow for small tests.

We read the it together with the description itself:

it("closes the modal when user confirms");

Is read as "it closes the modal when user confirms"

Examples:

  • it "shows an input element"
  • it "changes the color when user clicks button"
  • it "disables the button when required input is missing"

Recommended rules

1. Use uncomplicated language

In general: Uncomplicated, user-centered language without unneeded implementation-related context makes test readable. It helps others to understand your code better. Keep in mind that not all people are native english speaker!

It’s shorter, clearer, more emphatic and to-the-point.

️✅ Do

  • it("filters a list based on user input")

⛔️ Don't

  • it("a list is filtered based on string submitted by the user")
3. Use simple present tense for expected results

It’s a good way to express general truths or unchanging situations in an easy-to-read way.

️✅ Do

  • it("warns the user when they click delete")

⛔️ Don't

  • it("will warn the user when they clicked delete")
4. Use direct and unambiguous language

Don’t use modal verbs! Make direct and unambiguous statements about the expected outcome. The test is not to make sure code should (hopefully) do something. The test makes sure the code does this.

️✅ Do

  • it("shows list in the right order")
  • ⛔️ Don't
  • it("should show list in the right order")
5. Use user-based language for React component tests

For React component tests, try to focus on a more user-based language. Don’t add unnecessary implementation details to the descriptions.

️✅ Do

  • it("shows a list of topics filtered by team")

⛔️ Don't

  • it("should call the getTopicByTeam api to fetch new list items")