Introduction to Front-end Testing

Black box & white box

  • Black box testing, also known as functional testing, requires testers to look at the program as a whole, regardless of its internal structure and characteristics, and only verify that the program works as expected
  • White box testing is based on the code itself, generally refers to the logical structure of the code testing.

Test categorization

Unit Testing

Unit testing refers to testing the smallest testable unit of a program, such as testing a function, a module, a component…

Integration Testing

The encapsulation of high-level functions or classes exposed by composite integration of tested unit test functions

E2E Testing

Simulate user behavior, such as clicking, typing, etc. Then observe whether the elements on the page appear as expected. To determine whether or not they passed the test

TDD&BDD

TDD is Test-Driven Development

The philosophy of TDD is to write unit test case code before developing functional, business code

BDD is behavior-Driven Development

Developers and testers work together to analyze the requirements of the software and then write stories about those requirements. It’s the developer’s job to fill in the blanks and make sure the application works the way the user wants it to.

summary

TDD is about writing tests first and developing them later (usually unit tests, white box tests); BDD, on the other hand, is based on the user’s behavior and then writes test cases based on the user’s behavior (generally integration tests, black box tests).

Why Jest

The execution of automated tests is often supported by test specifications, assertions, mocks, coverage tools, and more, which are well implemented in the thriving Node.js ecosystem

  • Test frameworks: Provide some convenient syntax for describing test cases. Common test frameworks areJasmine.Mocha.Jest
  • Assertion library: Provides semantic methods for making various judgments about the values being tested. These semantic methods return the results of the test, either success or failure. Common assertion libraries areShould.js.Chai.jsAnd so on.
  • Test coverage tools: Used to count test cases against code and generate reports, such asistanbul.

But two problems arise when used together

  • There is a cost to choosing and learning multiple tools
  • The configuration of combining multiple tools into a specific test solution is complex

Jest is a testing framework developed by Facebook. Compared to other testing frameworks, Jest has several features:

  • Built-in common testing tools, such as built-in assertions, Mock, test coverage tools, out of the box.
  • As a front-end testing framework, Jest can use its unique snapshot function to automatically test Vue, React and other frameworks by comparing the snapshot files generated by UI code.
  • Jest’s test cases are executed in parallel, and only the tests corresponding to the changed files are executed, which is fast.

Jest installation

npm install jest --save-dev
Copy the code

Add script configuration in package.json

"scripts": {
    "test": "jest"
  }
Copy the code

This allows you to execute the test code using NPM Run test

The wheel test

Create a directory

├ ─ ─ the SRC │ └ ─ ─ math.h js ├ ─ ─ the test │ └ ─ ─ math.h test. The js ├ ─ ─ jest. Config. Js └ ─ ─ package. The jsonCopy the code

Create a SRC/math. Js

export function add(a, b) {
  return a + b;
}
Copy the code

Create the test/math. Test. Js


import { add } from "./math";
test("The add method".() = > {
  expect(add(1.2)).toBe(3);
});
Copy the code

Where expect(add(1, 2)).tobe (3) is an assertion that toBeJest calls a matcher

Execute test command

npm test
Copy the code

The default file matching rule matches.js files in the test folder (.jsx.ts.tsx also works). Matches all files with the.test.js or.spec.js suffix (.jsx.ts.tsx also works) Jest. Config. js file user-defined test file matching rules

Jest configuration file

Jest details can be customized in the jest.config.js file in the root directory, and jEST related configurations can also be contained in package.json, preferably in a separate configuration file for readability

Add jEST profile to existing project:

npx jest --init
Copy the code

Jest command line tool

Testing of asynchronous modules

Jest provides several testing support for asynchronous methods

//async.js
export const fetchDataCallback = (fn) = > {
  axios.get("http://www.dell-lee.com/react/api/demo.json").then((res) = > {
    fn(res.data);
  });
};

export const fetchData = () = > {
  return axios.get("http://www.dell-lee.com/react/api/demo.json");
};
Copy the code

The callback

The second function of the test method passes done to indicate that the callback is complete

//async.test.js
// Don't write that! The test ends without calling the callback function
test("fetchDataCallback".() = > {
  fetchDataCallback((res) = > {
    expect(res).toEqual({
      success: true}); }); });// Write it correctly
test("fetchDataCallback".(done) = > {
  fetchDataCallback((res) = > {
    try {
      expect(res).toEqual({
        success: true}); done(); }catch(err) { done(err); }}); });Copy the code

promise

test("fetchData".() = > {
  return fetchData().then((res) = > {
    expect(res.data).toEqual({
      success: true}); }); });Copy the code

Make sure that Promise is returned, otherwise the test session ends before the asynchronous method executes, and if you want to test resolve separately, you can use a different writing style

test("fetchData".() = > {
  return expect(fetchData()).resolves.toMatchObject({
    data: {
      success: true,}}); });Copy the code

async/await

The async/await test is easy, as long as the outer method is declared async

test("fetchData".async() = > {const res = await fetchData();
  expect(res.data).toEqual({
    success: true}); });Copy the code

Test case hook functions

Writing test cases often requires some pre-execution before running the test and some cleanup after running the test, and Jest provides helper functions to handle this

One-time setting

Use beforeAll and afterAll if the related task needs to be executed globally only once

beforeAll(() = > {
  init()
});

afterAll(() = > {
  clear()
});

test("addOne,".() = > {
  counter.addOne();
  expect(counter.number).toBe(1);
});

test("minusOne,".() = > {
  counter.minusOne();
  expect(counter.number).toBe(-1);
});

Copy the code

Many times over

Use beforeEach and afterEach if you need to perform data initialization beforeEach test task and data cleanup afterEach test task

import Counter from "./hook";
let counter = null;

beforeEach(() = > {
 counter = new Counter();
});

afterEach(() = > {
  counter=null
});

test("addOne,".() = > {
  counter.addOne();
  expect(counter.number).toBe(1);
});

test("minusOne,".() = > {
  counter.minusOne();
  expect(counter.number).toBe(-1);
});

Copy the code

scope

By default, blocks of before and after can be applied to each test in the file. Tests can be grouped by the Describe block. When the before and after blocks are inside the Describe block, they only apply to tests within the Describe block


  
  describe("Group 1".() = > {
    beforeAll(() = >{})
    beforeEach(() = > {});
    afterRAch(() = >{})
    afterAll(() = >{})
    
    test("addOne,".() = > {
      counter.addOne();
      expect(counter.number).toBe(1);
    });

    test("addTwo,".() = > {
      counter.addTwo();
      expect(counter.number).toBe(2);
    });
  });

  describe("Group 2".() = > {
   beforeAll(() = >{})
    beforeEach(() = > {});
    afterRAch(() = >{})
    afterAll(() = >{})
    
    test("minusOne,".() = > {
      counter.minusOne();
      expect(counter.number).toBe(-1);
    });

    test("minusTwo,".() = > {
      counter.minusTwo();
      expect(counter.number).toBe(-2);
    });
  });
});
Copy the code

Jest in mock

In many cases, test cases need to work in the relevant environment, and JEST provides rich environment simulation support

A mock function

Mock a function using jest.fn() mock, which has a.mock attribute

test("returnCallback".() = > {
  // Create a mock function
  const func = jest.fn();
  // Simulate the function return value
  func.mockReturnValue("callbackDone");
  returnCallback(func);
  // Mock attribute. The function is called and returns the value information
  expect(func.mock.results[0].value).toBe("callbackDone");
});
Copy the code

The mock Ajax requests

Some functions rely on Axios to make asynchronous requests, and we want to mock axios without making requests when we actually test it

//ajax
export const getData = () = > {
  return axios.get("/api").then((res) = > res.data);
};
Copy the code
//test
import Axios from "axios";
// Mock axios does not actually send ajax requests
jest.mock("axios");

test("getData".async() = > {// The result of the simulated Ajax request is returned
  Axios.get.mockResolvedValue({ data: "hello" });
   // The implementation can also be simulated
  // axios.get.mockImplementation(() => Promise.resolve(resp));

  await getData().then((data) = > {
    expect(data).toBe("hello");
  });
});
Copy the code

Timer Mocks

For the test using the delay module, due to the delay time is not necessarily, each test has to wait, affecting the test efficiency, JEST can simulate the delay module, accelerate the test case execution

export const timer = (callback) = > {
  setTimeout(() = > {
    callback();
  }, 3000);
};
Copy the code
jest.useFakeTimers();
test("timer".() = > {
  const fn = jest.fn();
  timer(fn);
  / / fast forward 3000 ms
  jest.advanceTimersByTime(3000);
  expect(fn).toHaveBeenCalledTimes(1);
});
Copy the code

The mock module

Suppose one module references another module’s methods

import Util from "./util";
export const demoFun = () = > {
  const util = new Util();
  util.a();
  util.b();
};
Copy the code

Mock You can mock a module using jest. Mock (module name)

jest.mock("./util");
import Util from "./util";
import { demoFun } from "./demoFun";
test("Test demoFun".() = > {
  demoFun();
  expect(Util).toHaveBeenCalled();
  expect(Util.mock.instances[0].a).toHaveBeenCalled();
  expect(Util.mock.instances[0].b).toHaveBeenCalled();
});
Copy the code

Jest. Mock discovers that Util is a class and automatically changes the constructor and method of the class to jest. Fn ()

const Util = jest.fn();
Util.init = jest.fn();
Util.a = jest.fn();
Util.b = jest.fn();
Copy the code

SnapShoot snapshot

There are often configuration files in a project. Such as:

//config.js
export const generateConfig = () = > {
  return {
    server: "http://localhost".port: 8080}; };Copy the code

So the test case that tests it could be written like this

Import {generateConfig} from './snapshot.js' test(' generateConfig', () => { expect(generateConfig()).toEqual({ server: 'http://localhost', port: '8080' })Copy the code

To constantly synchronize test cases as the configuration file grows, Jest provides snapshoot, which we can write like this

import { generateConfig } from './snapshot.js'

test("toMatchSnapshot".() = > {
  expect(generateConfig()).toMatchSnapshot();
});
Copy the code

ToMatchSnapshot () takes a snapshot of Expect’s result and matches it with previous snapshots, saving the currently generated snapshot if none existed, and updating the snapshot if the configuration file really needs to be changed

Suppose there are random variables in the configuration, as follows

export const generateConfig  = () = > {
    return {
        server: 'http://localhost'.port: '8080'.time: new Date()}Copy the code

Because each snapshot is different from the previous one, the test case will not pass, and you can use expect. Any () in case.

Import {generateConfig} from './snapshot.js' test(' generateConfig', () => { expect(generateConfig()).toMatchSnapshot({ time: expect.any(Date) })Copy the code

Before using Snapshoot, a single snapshot file was generated. Another method, in-line snapshot, requires pretter to be installed

npm install pretter --save
Copy the code

Modify test cases

test("toMatchInlineSnapshot".() = > {
  expect(generateConfig()).toMatchInlineSnapshot({
    date: expect.any(Date)}); });Copy the code

The test case is run, and the snapshot is saved to the test case code

import { generateConfig } from "./snapshot.js";
 
test("Test generateConfig".() = > {
  expect(generateConfig()).toMatchInlineSnapshot(
    {
      time: expect.any(Date)},` Object { "port": "8080", "server": "http://localhost", "time": Any
      
       , } `
      
  );
});
Copy the code

Test coverage report

Jest also provides support for test coverage by executing the command NPM test — –coverage or configuring package.json

"scripts": {
    "test": "jest"."coverage": "jest --coverage"
  }
Copy the code

Run the test commandThe command is added to the root directory of the projectcoverageFolder, open using a browsercoverage/lcov-report/index.htmlFiles with visual test reports

Jest tests are integrated in Vue

Carrying test environment

For new projects, the vue-CLI tool can be used to create projects

Just select Unit Testing Unit tests when selecting the configuration and select JEST as the Testing framework

For existing projects (for @vue/ CLI) you want to add jEST test modules. Running the following command line will help us install the JEST module.

vue add unit-jest
Copy the code

Jest configuration

After integrating Jest, a Jest.config.js file is generated in the root directory. And configured @vue/cli-plugin-unit-jest, this default. The default configuration is as follows.

odule.exports = {
  moduleFileExtensions: [
    "js"."jsx"."json".// tell Jest to handle *.vue files
    "vue"].transform: {
    // process *.vue files with vue-jest
    "^.+\\.vue$": require.resolve("vue-jest"),
    ".+\\.(css|styl|less|sass|scss|svg|png|jpg|ttf|woff|woff2)$": require.resolve(
      "jest-transform-stub"
    ),
    "^.+\\.jsx? $": require.resolve("babel-jest")},transformIgnorePatterns: ["/node_modules/"].// support the same @ -> src alias mapping in source code
  moduleNameMapper: {
    "^ @ / (. *) $": "<rootDir>/src/$1"
  },
  testEnvironment: "jest-environment-jsdom-fifteen".// serializer for snapshots
  snapshotSerializers: ["jest-serializer-vue"].testMatch: ["**/tests/unit/**/*.spec.[jt]s? (x)"."**/__tests__/*.[jt]s? (x)"].// https://github.com/facebook/jest/issues/6766
  testURL: "http://localhost/".watchPlugins: [
    require.resolve("jest-watch-typeahead/filename"),
    require.resolve("jest-watch-typeahead/testname")]};Copy the code

This default may not be sufficient, so we can extend it a bit

Mount components

To test the Vue component behavior, first start the rendering process, which in Vue means first mount the component. To mount a component, you need to convert the component to a constructor. The VUE component option is just a plain JavaScript object. A Vue constructor can be created from the options using the vue.extend method:

import Vue from 'vue';
import TodoList from ".. /todoList";
const Cons = Vue.extend(TodoList);

Copy the code

You can now create an instance using the new operator:

const vm = new Cons();
Copy the code

When Vue creates an instance, it does not automatically mount the DOM node. You need to manually call the $mount method:

const vm = new Cons().$mount()

Copy the code

When mount is called, vue generates DOM nodes, which can be accessed using the el attribute in the instance:

expect(vm.$el.textContent).toContain('todoList')
Copy the code

Jest runs test cases in a virtual browser environment created by the JsDOM library, so unit tests for vUE components, while creating a DOM tree, do not have to be run in a browser environment.


import TodoList from ".. /todoList";
import Vue from "vue";

describe("todolist.vue".() = > {
  const msg = "todoList";
  const  Cstor = Vue.extend(TodoList);
  const vm = new  Cstor().$mount();
  it("render".() = > {
    expect(vm.$el.textContent).toContain(msg);
  });
});

Copy the code

Run the test case

npm run test:unit
Copy the code

Vue Test Utils

To mount components, you need to create your own constructors and manually mount them. Vue Test Utils, vUE’s official unit testing utility library, makes unit testing of VUE components much easier. It contains helper methods to mount, interact with, and assert component output.

The mount mount

Vue Test Utils exports the mount method, which, upon receiving a component, mounts it and returns a wrapper object containing the mounted component instance VM. Wrappers are not just instance VMS, but also helper methods. One such method is text, which returns the textContent of the instance.

 import { mount } from "@vue/test-utils";

 it("renders msg when mounted".() = > {
   const nsg="todoList"
   // Use the mount method to mount components
   const wrapper = mount(TodoList);
   The text method returns all the text rendered by the component
   expect(wrapper.text()).toContain(msg)
 });

Copy the code
ShallowMount mount

ShallowMount is also a method of mounting components. Unlike mount, which renders only one layer of the component tree, shallowMount ensures that a component is tested independently of the component’s rendering output

Selection of test scheme

TDD+ unit testing

  • TDD is commonly used in conjunction with unit testing and is white box testing
  • Write the test cases first, then the code
  • Testing focuses on the code
  • High test coverage
  • Large amount of code, especially when testing VUE components, high coupling degree with business code
  • Too independent, can not guarantee the combination of modules is still good, low security

BDD+ integration testing

  • Write the code before you write the test cases
  • BDD is commonly used in conjunction with integration testing, such as black box testing
  • Test coverage is relatively low
  • Testing focuses on THE UI (DOM)

Summary:

It is not necessary to have only one test method in a project, but can be combined according to the actual situation. TDD+ unit test can be adopted for tool functions, tool classes and tests focusing on code logic; BDD+ integration test can be adopted for tests focusing on UI (DOM) of Vue single file components. Rational use of efficient testing methods can speed up development, improve code quality, as early as possible to find and remove bugs in the code.