What Are the Types of Front-end Testing

In fact, there are three types of front-end tests, namely E2E, integration, and single test. Other functional tests, UI tests, and interface tests are just one of them. In the test model, there are 4 test categories. Let me talk in-depth about the differences between these test types, their meaning, and how to optimize them.

End-to-end testing: Use a bot that closely resembles user behavior to interact with the app and verify that it functions properly. It is also sometimes called "functional testing" or E2E.

Integration testing: Verify that multiple units work together in harmony.

Unit tests: Verify that individually isolated sections work correctly.

Static tests: catch typos and type errors when writing code.

End-to-end Testing

In general, it will run the entire application (frontend + backend) and such tests will interact with the application like a real user. The following example is implemented using Cypress.

import {generate} from 'todo-test-utils'

describe('todo app', () => {
  it('should work for a typical user', () => {
    const user = generate.user()
    const todo = generate.todo()
    // Here we will go through the entire registration process
    // I usually just write a test to do this
    // The rest of the tests will implement the registration function by directly sending HTTP requests
    // This way we can skip the interaction with this registration form
    cy.visitApp()

    cy.findByText(/register/i).click()

    cy.findByLabelText(/username/i).type(user.username)

    cy.findByLabelText(/password/i).type(user.password)

    cy.findByText(/login/i).click()

    cy.findByLabelText(/add todo/i)
      .type(todo.description)
      .type('{enter}')

    cy.findByTestId('todo-0').should('have.value', todo.description)

    cy.findByLabelText('complete').click()

    cy.findByTestId('todo-0').should('have.class', 'complete')
    // and many more...
    // My E2E tests are generally written like real users
    // sometimes very long
  })
})

Integration Testing

The idea behind integration tests is to have as few mocks as possible. I usually only Mock the following two points.

Network request (with MSW)

Components that implement animations (because who wants to wait in tests)

The test case below renders the entire application, but this is not a hard requirement for integration testing. And most of the integration tests I write don't render the entire app. They generally only render the Providers used in the App, which is what the render in the test/app-test-utils pseudo-module does).

import * as react from 'react'
import {render, screen, waitForElementToBeRemoved} from 'test/app-test-utils'
import userEvent from '@testing-library/user-event'
import {build, fake} from '@jackfranklin/test-data-bot'
import {rest} from 'msw'
import {setupServer} from 'msw/node'
import {handlers} from 'test/server-handlers'
import App from '../app'

const buildLoginForm = build({
  fields: {
    username: fake(f => f.internet.userName()),
    password: fake(f => f.internet.password()),
  },
})

// Integration tests generally only use the MSW library to Mock HTTP requests
const server = setupServer(...handlers)

beforeAll(() => server.listen())
afterAll(() => server.close())
afterEach(() => server.resetHandlers())

test(`logging in displays the user's username`, async () => {
  // This custom render will return a Promise when the app is loaded
  // (if you use server rendering, you probably don't need to do this)
  // This custom render also lets you specify your initial route
  await render(<App />, {route: '/login'})
  const {username, password} = buildLoginForm()

  userEvent.type(screen.getByLabelText(/username/i), username)
  userEvent.type(screen.getByLabelText(/password/i), password)
  userEvent.click(screen.getByRole('button', {name: /submit/i}))

  await waitForElementToBeRemoved(() => screen.getByLabelText(/loading/i))

  // Check if the user is already logged in
  expect(screen.getByText(username)).toBeInTheDocument()
})

For such tests, I usually do some global processing, such as automatically resetting all Mocks.

Unit Testing

import '@testing-library/jest-dom/extend-expect'
import * as React from 'react'
// if your integration test has a test harness module like above
// Then don't use @testing-library/react, just use yours
import {render, screen} from '@testing-library/react'
import ItemList from '../item-list'

// Some people may not call such a test a single test, because we still use React to render into the DOM
// they may also tell you to use shallow render
// When they tell you this, slap them in the face with this link https://kcd.im/shallow
test('renders "no items" when the item list is empty', () => {
  render(<ItemList items={[]} />)
  expect(screen.getByText(/no items/i)).toBeInTheDocument()
})

test('renders the items in a list', () => {
  render(<ItemList items={['apple', 'orange', 'pear']} />)
  // Note: to simplify this example, snapshots are used here, but only:
  // 1. The snapshot is very small
  // 2. We use toMatchInlineSnaphost
  // Details: Read more: https://kcd.im/snapshots
  expect(screen.getByText(/apple/i)).toBeInTheDocument()
  expect(screen.getByText(/orange/i)).toBeInTheDocument()
  expect(screen.getByText(/pear/i)).toBeInTheDocument()
  expect(screen.queryByText(/no items/i)).not.toBeInTheDocument()
})

I believe everyone knows that the following must be a single test.

// Pure functions are the best choice for unit testing, I also like to use jest-in-case for unit testing
import cases from 'jest-in-case'
import fizzbuzz from '../fizzbuzz'

cases(
   'fizzbuzz',
   ({input, output}) => expect(fizzbuzz(input)).toBe(output),
   [
     [1, '1'],
     [twenty two'],
     [3, 'Fizz'],
     [5, 'Buzz'],
     [9, 'Fizz'],
     [15, 'FizzBuzz'],
     [16, '16'],
   ].map(([input, output]) => ({title: `${input} => ${output}`, input, output})),
)

Static Test

This actually refers to the use of static inspection tools such as TypeScript and ESLint to find code problems.

// Can you spot the following problem?
// I believe ESLint's for-direction rules are better than when you code review
// Find the problem faster :wink:
for (var i = 0; i < 10; i--) {
   console.log(i)
}

const two = '2'
// This is a bit finicky, but TypeScript will tell you it's bad
const result = add(1, two)

Summary

Each level in the model has its own advantages and disadvantages. An E2E test can fail many times, so it's hard to track down which code is causing the crash. But it also means it can give you more confidence. Such tests are useful when you don't have time to write tests. I'd rather face E2E tests that fail multiple times to gain more confidence in the code than have to deal with more bugs for not writing them.

In the end, I don't really care about the difference between these test types. If you say my unit tests are integration tests or even E2E tests, say so. What I care more about is whether they can give me enough confidence to change the old code and realize new business. So I would combine different testing strategies to achieve this goal.

Well, this foreign language is brought here for you. The article mainly talks about 4 test types: static, single test, integration, and E2E. In fact, in the process of writing tests, it is difficult to distinguish which test you are writing, and you don't have to think about these problems all the time. For example, what type of test I am writing, how to divide the proportion of project test types, and how many tests are there.



Leave a reply



Submit