There are thousands of articles on the topic of “automated testing,” but a closer look reveals that a large number of them are back-end or BFF tests that can cover nearly 100% of the tests (depending on the team’s requirements).

But when it comes to front-end testing, there are few who say they can achieve 100% coverage. Of course, there are reasons for this. The FRONT-END UI changes too quickly and is constantly tweaking. Most of the front ends Say NO.

Today this is a front-end test collection, from the test pyramid principle to analyze the front-end test includes several types of testing, introduced the cost performance of each type of testing, the use of scenarios and testing frameworks or tools commonly used by each type of testing. Because the unit test is often the largest proportion of automated tests, so from two aspects of how to write a good unit test, how to use a reasonable test surrogate isolation test dependence, improve the independence of the test. It is expected that the development and practice of front-end testing strategy can help improve the software quality and reduce the “eight (B) a (U) G”.

Test the Pyramids

The testing pyramid in Succeeding With Agile by Mike Cohn divides testing into three layers: UI testing, Service testing and unit testing. The higher the cost is, the lower the efficiency is. Therefore, it is recommended to write more test cases from the bottom up. This is not entirely the case from today’s technology perspective, as testing of the front-end UI layer is much less expensive than it used to be, such as testing UI snapshots, testing key DOM elements that rely on the front-end framework, and so on.

Now, the most referential points of the whole test pyramid are:

  • (1) Test granularity is different at different levels
  • (2) The higher the level, the fewer tests are written because it is less cost-effective.

So a more appropriate front-end test pyramid for now would look something like:

  • (1) E2E test

    Some teams also include E2E testing in the front end to ensure stable functional availability, but the cost of E2E testing is higher than that of the other 2 layers, so as far as possible, the main process functions are tested in E2E.

  • (2) Integration test

    When encountering complex front-end businesses, state management is often introduced to trigger state changes from DOM operations and cause re-render, which can ensure the correctness of the whole process through integration testing.

  • (3) Unit test

    Speaking of unit testing, back-end or BFF unit testing, everyone is easy to understand, but what does front-end unit testing generally involve? Unit tests apply to Util methods, which are pure functions, and unit tests for pure functions are easy to write because each input and output are specified. Of course, in addition to the above, there are also data processing unit tests in state management, UI level unit tests, and so on. Specific test writing follow-up to see specific cases.

Common front-end testing methods

With the testing pyramid introduced, what are some of the most common testing methods used in the front-end domain? Here are some common test methods in practice:

Unit testing

In the front end, a unit can be a UI component, an Util method, a state management handler, a business logic function, and so on, all of which can be unit tested to ensure functionality.

The following is a test case from the Jest documentation, which tests the SUM method. In a real project, there are often many custom methods written, and the responsibilities of each method should be very clear. It is possible to test each scenario after the input and output of the method.

function sum(a, b) {
  return a + b;
}
module.exports = sum;
Copy the code
const sum = require('./sum');

test('adds 1 + 2 to equal 3'.() = > {
  expect(sum(1.2)).toBe(3);
});
Copy the code

A snapshot of the test

Snapshot tests, as the name suggests, are tests against snapshots. In the front-end test framework, a snapshot test is usually generated at the first execution. Take Jest as an example. The React snapshot test is as follows:

import React from 'react';
import renderer from 'react-test-renderer';
import Link from '.. /Link.react';

it('renders correctly'.() = > {
  const tree = renderer
    .create(<Link page="http://www.facebook.com">Facebook</Link>)
    .toJSON();
  expect(tree).toMatchSnapshot();
});
Copy the code

Specific jest for reference in the document snapshot test (www.jestjs.cn/docs/snapsh)…

During the first run of the above test, the following snapshot file is created, saving the snapshot results of the first run. If a component changes and you run the snapshot test again, a new snapshot result is generated. The new snapshot result is compared with the saved snapshot file. If the snapshot result is inconsistent, the snapshot test fails. Of course, testing frameworks usually provide commands to update snapshots, such as the U command in the Jest framework.

exports[`renders correctly 1`] = `  Facebook  `;
Copy the code

Based on the analysis of the snapshot tests above, it is not appropriate to add snapshot tests to UI components that are still in development, because almost every time a snapshot is updated to pass the test, the point of snapshot tests is lost. Therefore, snapshot tests are generally applied to stabilized front-end pages or components to avoid erroneous changes.

The test of contract

Many teams today are front-and-back teams that spend a lot of time on tuning (for example, interface availability? Is the interface documented? Is the interface stable? . The proposal of contract test is a contract based on the provider and consumer of the interface. This contract is generally an interface specification document, which usually contains request URL, Method, request parameters, response data structure, etc. In the scenario of front-end and back-end separation, the contract test is more inclined to the API test between services, mainly to understand the dependency between services and speed up the API verification.

As the consumer of contracts, the front end often creates corresponding unit tests, in which a request conforming to contract specifications is sent to the Mock Server to obtain the corresponding response to verify the results. When there is no Mock Server, The front-end can also add its own contract-compliant mock Response files to decouple the API. Common tools include yamL-based Swagger Specification and JSON-based Pact Specification, as documented in their documentation.

  • Swagger Specification: Swagger. IO/specificati…
  • Specification:github.com/pact-founda Pact…

E2E test

E2E testing, also known as end-to-end testing, is more functional automated testing. This type of test often simulates a real user action to verify that the results are as expected. E2E tests are usually black-box tests that focus on whether the entire system meets user expectations.

There are many tools available for the above testing methods, which can be found in the next chapter.

Front-end testing tool

Testing tools include Test Framework, Assertion Library, Mock Library, and Test report tool Library.

Here are some libraries commonly used for front-end testing, along with official documentation.

Testing Library

The Test Library is a collection of Test libraries. It simulates user interaction and verifies the external state changes of components for testing. In this way, it can improve the efficiency of testing without going into the implementation details of components. The React Testing Library includes vue Testing Library and Angular Testing Library.

React Testing Library + Jest is the recommended test solution for React Testing.

We recommend using the React Testing Library, which makes it as easy to write test cases against components as if they were used by the end user. When using the React version <= 16, use the Enzyme test tool to easily assert, manipulate, and traverse the React component’s output.

The React Testing Library is a Testing Library that you can use to write maintainable Tests of React components without having to pay attention to the detailed internal implementation of the React component. The React Testing Library provides a number of functions to locate elements that can be used for assertions or user interactions. For specific usage, please refer to the documentation.

Reference links:

  • React Testing Library: testing-library.com/react
  • The Test Library:github.com/testing-lib…
  • Enzyme: an IO/Enzyme /

Jest

Jest is an open source testing framework for Facebook. It provides built-in JSDOM runtime environment, assertion library, coverage, snapshots, and Mock Funtion. It is one of the most widely used front-end testing frameworks. If you initialize the React framework using the Create React App, it already has Jest built in as its test library. If you build your own framework from scratch, it’s easy to introduce Jest according to the official documentation. I won’t go into details here, but Jest is very easy to get started with, and you can check out the Jest documentation if you need to.

Reference links:

  • IO/zh-hans /doc…
  • Create React App test document: create-react-app.dev/docs/runnin…

Mocha

Mocha is a flexible JS-based testing framework that includes asynchronous handling (beforeEach, afterEach and other hook functions, Promise handling, timeout handling, and so on), concise test reports, and custom assertion tools:

  • Should.js-bdd style throughout (github.com/shouldjs/sh…)
  • Expect. Js-expect () Style assertion (github.com/LearnBoost/…)
  • Chai – Expect (), Assert () and Should-style assertions (chaijs.com/)
  • Self-documented assert() in the style of better-assert-c. (github.com/visionmedia…).
  • Unexpected – “Extensible BDD assertion tool”. (unexpected.js.org/)

All of these assertions can be customized.

Jasmine

Angular’s default testing framework is Karma (a front-end test run framework developed by the Google team) + Jasmine.

Jasmine is a full-featured testing framework with complete assertion methods, Setup and Teardown methods, asynchronous processing, Mock functions, and more. Jasmine documentation link: github.com/jasmine/jas…

Cypress

Cypress is an E2E testing framework developed on the basis of Mocha API. It does not rely on the front-end framework, nor does it need other testing tool libraries. It is simple to configure and provides powerful GUI graphics tools, which can automatically capture pictures and record the screen, and can also Debug in the testing process. It is a popular end – to – end testing tool. For details, please refer to the documentation and write use cases according to the documentation. Cypress documentation link: www.cypress.io/

There are many other libraries, such as Jasmine, Selenium, Puppeteer, and Phantomjs, which are not listed here. If you need them, you can use the documentation. For more libraries, you can see another player-related library (github.com/huaize2020/…). .

Front-end testing strategy

With a lot of UI interaction on the front end, this part of the test is relatively cost-effective because the DOM structure changes frequently, so you can consider snapshot testing if you really need it.

In the front-end project, it is necessary to test the logic part of the front-end code. Now the front-end is not only UI, but also includes state management and business logic, which can be guaranteed by unit testing.

You can also consider writing integration tests as part of your test strategy, such as simulating a DOM operation, triggering a state change and re-rendering, and finally verifying that the render results are as expected. This type of testing is also a type of black box testing, because the logic of state changes and re-render is not exposed in the test, but rather the function is guaranteed by triggering actions to verify the final result.

Of course, different projects or to consider the particularity of the project, according to the focus of different testing and cost to consider what kind of testing strategy.

How to write good unit tests

There are two ways to write unit tests. The business and technical perspectives are not in conflict, but the dimensions of thinking are different:

Write unit tests from a business perspective

A unit test can be described from a business perspective using Given\When\Then, which means what premises are provided (typically to prepare test data), When what is done (typically to invoke specific methods), and Then what happens (typically to test assertions).

Consider a specific case: a unit test is defined with its own description and test content. The test contents are divided into preparing products data (given), calling getTotalAmount tested function (when), and expecting predicate to calculate result result (then).

/ / implementation
const getTotalAmount = (products) = > {
  return products.reduce((total, product) = > total + product.price, 0); 
}
Copy the code
/ / test
it('should return total amount 600 when there are three products priced 100, 200, 300'.() = > {
  // given - Prepare data
  const products = [
    { name: 'nike'.price: 100 },
    { name: 'adidas'.price: 200 },
    { name: 'lining'.price: 300},]// when - Calls the function under test
  const result = getTotalAmount(products)

  // then - asserts the result
  expect(result).toBe(600)})Copy the code

Write unit tests from a technical perspective

Technically, there are four stages of testing: Setup, Exercise, Verify and Teardown.

Most test frameworks have many corresponding methods for the four phases. Here is an example: Prepare for tests by beforeEach. In a single test, execute specific methods to be tested, verify test results with Expect assertion, and clear data after the test to avoid impact on other tests. Data to be cleared in Teardown is generally shared by different tests.

/ / test
describe("getTotalAmount test".function () {   
    let products=[]
    // Setup
    beforeEach(function () {        
        products = [
          { name: 'nike'.price: 100 },
          { name: 'adidas'.price: 200 },
          { name: 'lining'.price: 300}]});// Teardown
    afterEach(function () {        
      products=[] 
    });       

    it("should return total amount 600 when there are three products priced 100, 200, 300".function () {
        // Exercise
        const result = getTotalAmount(products)
        // Verify
        expect(result).toBe(600)}); });Copy the code

Test double

Test Doubles are used to isolate dependencies that affect testing, such as third-party UI components, third-party utility classes, interfaces, and so on. With dependencies isolated, you can focus on the functionality of the front end itself and keep your code functional. Test surrogates fall into the following categories:

Test Stub

Stubs provide canned answers to calls made during the test, usually not responding at all to anything outside what’s programmed in for the test.

The Test Stub provides a wrapped response for each invocation and generally does not respond to requests outside of the Test. Stub is often translated as “pile”, in testing, often with a Stub to completely replace the component under test (system) of dependent objects, we give it sets the output (the return value), it is like being measured components (system) in a pile, only used for testing, we won’t go to verify the pile internal logic will not go to verify whether is invoked. All the test needs is its impact, the default return value.

Jest.fn (), for example, is a Stub that can mock out its return value or return a undefined method by default.

const myObj = {
  doSomething() {
    console.log('does something'); }}; test('stub .toHaveBeenCalled()'.() = > {
  const stub = jest.fn();
  stub();
  expect(stub).toHaveBeenCalled();
});

test('Stub jest.fn() return value'.() = > {
  let stub = jest.fn().mockReturnValue('default');
  expect(stub()).toBe('default');
})
Copy the code

Test Spy

Spies are stubs that also record some information based on how they were called. One form of this might be an email service that records how many messages it was sent.

As described in the figure below, Test Spy refers to the use of Test surrogates to capture calls made by the system under Test to other components for verification in subsequent tests. The biggest difference between this and Mock is that it is “catch”. Mock is to throw an Object at Mock setUp, whereas Spy catches the call.

For example, in the following case, we add incrementSpy test surrogate, catch counter’s increment method call, and verify that the test surrogate was actually called once at test time (regardless of the implementation in INCREMENT).

let count = 0;
const counter = {
  increment() {
    count += 1;
  },
  getCount() {
    returncount; }};const app = counter= > {
  counter.increment();
};

test('app() with jest.spyOn(counter) .toHaveBeenCalledTimes(1)'.() = > {
  const incrementSpy = jest.spyOn(counter, 'increment');
  app(counter);
  expect(incrementSpy).toHaveBeenCalledTimes(1);
});
Copy the code

Mock Object

Mocks are pre-programmed with expectations which form a specification of the calls they are expected to receive. They can throw an exception if they receive a call they don’t expect and are checked during verification to ensure they got all the calls they were expecting.

Replace the test dependent objects with Mock objects that only verify that they are called correctly during the test. As shown in the diagram below, the Mock Object predetermines that it will receive calls during coding and checks for all expected calls during validation. If it receives an unexpected call, it will throw an exception.

For example, in Jest, we Mock a counter object and use the counter object as an input parameter to isolate the impact of counter on the test. In Jest, we check whether increment of counter is called correctly. It doesn’t care about the implementation of counter increment.

let count = 0;
const counter = {
  increment() {
    count += 1;
  },
  getCount() {
    returncount; }};const app = counter= > {
  counter.increment();
};

test('app() with mock counter .toHaveBeenCalledTimes(1)'.() = > {
  const mockCounter = {
    increment: jest.fn()
  };
  app(mockCounter);
  expect(mockCounter.increment).toHaveBeenCalledTimes(1);
});
Copy the code

Fake Object

Fake objects actually have working implementations, but usually take some shortcut which makes them not suitable for production (an InMemoryTestDatabase is a good example).

Fake Object is a way to replace components relied on in testing with simpler, lighter implementations. A typical Fake Object for back-end testing is an in-memory database that simulates real database operations, but this kind of Fake Object is not suitable for use in a production environment and is generally used only for testing. In summary, Fake Object sounds very similar to the positioning of the Test Stub, but Fake Object is a lighter implementation of the dependency component. It only provides the same interface as the dependency component to be called by the system (component) under Test. Different values are returned for different test scenarios.

In front-end testing, it is common to rely on other components or systems, such as UI component libraries that reference third parties. In our tests, however, we didn’t care about the implementation of the third-party UI library. These can be Fake objects.

For example, the React framework is often used in conjunction with Redux. When testing a component that connects to Redux, you can replace Redux with redux-mock-store, a lightweight testing library.

import configureStore from 'redux-mock-store';
 
const mockStore = configureStore([]);
 
describe('My Connected React-Redux Component'.() = > {
  let store;
 
  beforeEach(() = > {
    store = mockStore({
      myState: 'sample text'}); }); it('should render with given state from Redux store'.() = >{}); it('should dispatch an action on button click'.() = >{}); });Copy the code

Dummy Object

Dummy Object is a Dummy parameter passed to a method for testing purposes. A Dummy Object passed to a method as a parameter serves no other purpose than to successfully call the method under test.

For example, if the front-end util needs to pass through a parameter that is not actually used, but only used for pass-through, then the Object passed but not actually used is Dummy Object.

The above test doubles in practice depends on the specific situation to use, different schools have different use schemes. In testing, when there are component dependencies, the test is state verification or behavior verification. Stub and Fake Object are generally used for state authentication, which does not care about the number of times it is invoked. For behavioral validation, you would typically use Spy or Mock to validate the call results.

Related documentation links for testing surrogates:

  • The Test Doubles:xunitpatterns.com/Test%20Doub…
  • Redux-mock-store:github.com/reduxjs/red…

conclusion

The types of front-end tests are roughly the ones mentioned above, with unit tests having the highest ratio, because each unit test costs less than other tests and can test various scenarios more comprehensively. The Test Library is a good choice for testing components related to the DOM. The specific tool to use for testing can be based on the project situation (combined with the project technology selection and testing strategy). Of course, the test framework is only a tool for writing tests, and the test responsibilities of each test case should be clearly defined to cover comprehensive test scenarios as far as possible, so as to avoid errors and function failures caused by subsequent reconstruction and improve software quality.

The last

Search Eval Studio on wechat for more updates.