Front end automated testing
Why do you need automated tests
After continuous development, the project will be stable eventually. The introduction of automated testing at the appropriate time can find problems early and ensure the quality of products.
Benefits of automation = number of iterations * cost of full manual execution - cost of first automation - Number of maintenance * Maintenance costCopy the code
test
As the last link in the complete development process, testing is an important link to ensure product quality. However, front-end testing is generally a later part of the product development process and a higher level in the whole development architecture. Front-end testing is more inclined to the characteristics of GUI, so front-end testing is very difficult.
The test method
Black box testing
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. Black box testing is closer to the real world of user use because the inside of the program is not visible to the user.
Here are some common methods for black-box testing:
-
Equivalence division
Equivalence class division is mainly to design test cases by determining the interval between legal input and illegal input in the case of existing input rules
The password for logging in to the website must consist of six digits
Valid equivalence class: 6 digits
Invalid equivalence classes: bits >6, bits <6, full-corner digits, letters, special characters, etc…
-
Boundary value analysis
As the name implies, the design of test cases is mainly based on the boundary values of input and output ranges. The reason is that a large number of errors often occur at the boundary of the input or output range (programmers tend to make mistakes in these places). Boundary value analysis is generally used in combination with equivalence class partition, and equivalence class partition interval boundary is generally the boundary value.
For example, the login password must contain 6 to 12 characters
Valid equivalence classes: bits [6-12]
Invalid equivalence classes: bits <6 bits >12
Boundary value: 6 12
-
Error prediction, anomaly analysis, etc
Black box testing also includes some other methods of testing. Since testing is often not exhaustive, how to design test cases to ensure that the test covers as many scenarios as possible not only depends on these summarized methods, but also tests the talent of the tester.
White box testing
White box testing is based on the code itself, generally refers to the logical structure of the code testing. White-box testing is testing with an understanding of the structure of the code. The goal is to iterate through as many executable paths as possible to get test data. There are many white-box testing methods, mainly logical coverage, that is, checking every line of code and every judgment result.
There are mainly the following logical coverage methods in order of error detection ability:
- Statement coverage (making the program execute to each line of statements)
- Judge override (make each judge statement true or false)
- Condition override (make each condition in each judgment statement true or false)
- Decision/Condition coverage (both 2 and 3)
- Condition combination overlay (determine that each combination of conditions in a statement occurs at least once)
- Path coverage (covering each execution path of the program)
Test categorization
According to the bottom-up concept of software engineering, front-end Testing is generally divided into Unit Testing, Integration Testing and E2E Testing. As you can see from the graph below, the complexity of bottom-up testing will increase, while the benefits of testing will decrease.
Unit Testing
Unit testing refers to the testing of the smallest testable unit in a program, and generally refers to the testing of functions. Unit testing is a mixture of programming and testing. Because it tests the internal logic of the code, it uses more white-box testing. Unit tests force developers to write more testable code, which is generally much more readable, and good unit tests serve as documentation for the code under test.
Function testability: Functions with high testability are generally pure functions, that is, functions with predictable inputs and outputs. That is, no incoming arguments are modified inside a function, no API or IO requests are executed, and no other impure functions such as math.random () are called
The biggest characteristic of unit test is the fineness of the test object, that is, the independence of the tested object is high and the complexity is low.
Front-end unit testing
The biggest difference between front-end unit testing and back-end unit testing is that front-end unit testing inevitably has compatibility problems, such as calling the browser compatibility API and calling the BOM (Browser Object Model) API. Therefore, front-end unit testing needs to be run in the (pseudo) browser environment.
In terms of the classification of test running environment, there are mainly the following test schemes:
- Based on theJSDOM
- Advantages: Fast, fastest execution, because no browser startup is required
- Disadvantages: Related operations such as seesion or cookie cannot be tested, and the correctness of some Dom related and BOM related operations cannot be guaranteed because it is not a real browser environment, and JSDOM does not implement localStorage. If overwriting is required, Emulation can only be performed using third-party libraries such as Node-localStorage (which itself has some problems with the execution environment).
- Based on thePhantomJsWait for headless browsers
- Advantages: Relatively fast and with a real DOM environment
- Disadvantages: It also does not run in a real browser, it is difficult to debug, and there are many project issues. After puppeteer is released, the author announces that puppeteer will not be maintained
- Use a tool such as Karma or Puppeteer to call a real browser environment for testing
- Advantages: Simple configuration, running tests in real browsers, and Karma can run test code in multiple browsers, making it easy to debug
- Disadvantages: The only disadvantage is that it runs slightly slower than the first two, but within the acceptable range of unit tests
Front-end unit testing tools
The front end has mushroomed in recent years with numerous testing frameworks and related tools.
- The test platform
- Karma – The Test run platform developed by the Google Angular team is simple and flexible to configure, making it easy to run tests in multiple real browsers.
- The test framework
- Mocha — Tj is an excellent testing framework developed by Mocha — Tj. It has a complete ecosystem, simple test organization, no restrictions on assertion libraries and tools, and is very flexible.
- Jasmine – Very similar to Mocha syntax, the biggest difference being that it provides custom assertions and Spy and stubs
- Jest – A large and comprehensive testing framework developed by Facebook and recommended by React. It is easy to configure and fast to run. (Note: Cannot integrate with Karma)
- AVA – The biggest difference from the above testing framework is that it is multi-threaded and runs faster.
- Other – There are some other front-end testing frameworks, but they are quite similar, except for the different integration of tools such as assertions and test piles. Mocha is recommended if stability and maturity are considered, and JEST and AVA are considered if there are very high requirements for test running speed
- Test AIDS
- Assertion Library – Chai If unit tests are not running in a real browser environment, it is possible to simply use Node assert, but Chai is recommended as the assertion library (there are multiple TDD and BDD style assertions available, and the ecosystem is thriving).
- Test pile (also called double) – Sinon and testDouble tool provides such as the test pile, intercept simulation request, “time travel”, and other functions, is mainly used to solve the “function is not pure” (such as test if the callback is called right, XHR is correct by request, time delay function behavior is correct) test.
- Test coverage tool
- The Istanbul base implementation provides tools such as the command line, but does not solve the problem of code compilation
- Istanbul instrumental-loader Webpack reconfigures code and test report outputs
Other reference
- Chai-as-promise extends chai’s assertion capabilities on promises
- Sinon -chai extends assertions when chai is paired with sinon
- Chai -jquery extends CHAI assertions in UI tests
- The Istanbul website describes how Istanbul integrates with multiple testing frameworks and support for languages like Typescript
- This Istanbul tutorial introduces the concept of code coverage and the simple use of Istanbul with Mocha
Common unit testing chestnut
The following conditions or problems should be considered when selecting the frame:
- The test needs to run in a real browser
- Test execution needs to be fast enough
- The code under test is Typescript, so compilation and typing issues need to be addressed
- Facilitate continuous integration
The solution that uses Karma+Webpack+Mocha+Chai+Sion+ Istanbul -instrumenter-loader is selected.
Project Structure:
Karma can be easily integrated with Webpack by specifying the files to be precompiled and the compilation tools to use.
Karmap.conf.js configures Webpack compilation
module.export=funciton(config){
config.set({
frameworks: ['mocha', 'chai'],
files: [
'tests/setup.js'
],
preprocessors: {
'tests/setup.js': ['webpack']
},
webpack: {
resolve: {
extensions: ['.js','.ts'],
},
module: {
rules: [{
test: /\.ts$/,
loader: 'ts-loader',
query: {
transpileOnly: true
}
},
{
test: /\.ts$/,
exclude: /(node_modules|libs|\.test\.ts$)/,
loader: 'istanbul-instrumenter-loader',
enforce: 'post',
options: {
esModules: true,
produceSourceMap: true
}
}
]
},
devtool: 'inline-source-map',
},
// karma-server support ts/tsx mime
mime: {
'text/x-typescript': ['ts', 'tsx']
},
})
}
Copy the code
There are several points to note in the above configuration:
-
setup.js
// Import all test cases const testsContext = require.context(".. /src", true, /\.test\.ts$/); testsContext.keys().forEach(testsContext);Copy the code
Use require.context() provided by WebPack to import all test files uniformly, then karma calls Webpack with setup.js as the entry for compilation, and then the test executes.
-
Mime configuration support for TS/TSX
-
Istanbul – instruments-loader Loader call order must be before ts or Babel or it will not be accurate, excluding the test file itself and dependent library coverage calculation
- Use Enforce: ‘POST’ to ensure the sequence of loader calls
- Use exclude to exclude third-party libraries and test file coverage calculations themselves
Refer to Karma and Webpack, as well as the documentation for related plug-ins, for additional configurations
-
Pure function test
/** * export function mail(input: any): boolean { return /^[\w\-]+(\.[\w\-]+)*@[\w\-]+(\.[\w\-]+)+$/.test(input); Measured function} / * write write write write write write write write * / / * left left left left test code left left left left down down down down down down down * / the describe (' # validation mailbox mail, () = > {it (' incoming mail string, Returns true ', () = > {expect (mail (' [email protected] ')). To.. Be true; expect(mail('[email protected]')).to.be.true; expect(mail('[email protected]')).to.be.true; expect(mail('[email protected]')).to.be.true; expect(mail('[email protected]')).to.be.true; }); It (' incoming email strings, returning true ', () = > {expect (E-mail (' ')). To.. Be false; expect(mail('abc@')).to.be.false; expect(mail('@123.com')).to.be.false; expect(mail('abc@123')).to.be.false; expect(mail('123.com')).to.be.false; }); });Copy the code
-
Tests for promises or other asynchronous operations
Mocha supports asynchronous testing in three main ways:
-
Using async await
It (async ()=>{await asynchronous() // some assertions})Copy the code
-
Use the callback function
It (done = > {Promise. Resolve (), then (() = > {/ / assert that the done () / / test the callback after the identification test end})})Copy the code
-
Return a Promise
Promise resolve return promise.resolve ().then(()=>{// assert})})Copy the code
-
-
Tests with HTTP requests
HTTP (method, url, and the function body) {/ / using XHR sends an ajax request} / * write write write write function being measured write write write write * / / * left left left left test code left left left left down down down down down down down * / it (' for the GET method, (done) => { const xhr=fake.useFakeXMLHttpRequest() const request = [] xhr.onCreate = xhr => { requests.push(xhr); }; requests[0].respond(200, { "Content-Type": "application/json" },'[{ "id": 12, "comment": "Hey there" }]'); get('/uri').then(() => { xhr.restore(); done(); }); expect(request[0].method).equal('GET'); })Copy the code
Or encapsulate fakeXMLHttpRequest
/** * use the fakeXMLHttpRequest * @param callback function, */ export function useFakeXHR(callback) {const fakeXHR = useFakeXMLHttpRequest(); const requests: Array<any> = []; // Requests passes references to callback, so use const to avoid overwriting the pointer. fakeXHR.onCreate = request => requests.push(request); callback(requests, fakeXHR.restore.bind(fakeXHR)); } it (' for the GET method, (done) = > {useFakeXHR ((request, restore) = > {GET ('/uri). Then (() = > {restore (); done(); }); expect(request[0].method).equal('GET'); request[0].respond(); })})Copy the code
UseFakeXHR encapsulates the useFakeXMLHttpRequest for Sinon and implements the fake for XHR requests
Fake XHR and server-sinon. JS for reference
The difference between Fake XHR and Fake Server: The latter is a higher level of encapsulation of the former, and Fake Server is more granular
-
Time dependent tests
Callback function timer(delay,callback){setTimeout(callback,delay); Measured function} / * write write write write write write write write * / / * left left left left test code left left left left down down down down down down down * / it (' the timer, () = > {const clock. = sinon useFakeTimers () const spy = sinon. Spy () / / test double Timer (1000,spy) clock.tick(1000) // "time travel, go to 1000ms" expect(spy.called).to.be.true clock.restore() // Restore time, Otherwise it will affect other tests})Copy the code
-
Tests for browser-specific API calls
- The session of chestnuts
/** * WebStorage encapsulates @param storage objects, Support localStorage and sessionStorage */ export function Storage(Storage: Storage) {/** * get session * @param key * @return Return JSON parsed value */ function get(key: string) { const item = storage.getItem(key); if (! item) { return null; } else { return JSON.parse(item); } } } export default Storage(window.sessionStorage); Measured function / * write write write write write write write write * / / * left left left left test code left left left left down down down down down down down * / the describe (' session '() = > {beforeEach (' empty sessionStorage', () => { window.sessionStorage.clear(); }); Describe (' get session# get ', () = > {it (' access to simple type value, () = > {window. The sessionStorage. SetItem (' foo ', JSON.stringify('foo')) expect(session.get('foo')).to.equal('foo'); }); It (' access to a reference type value, () = > {window. The sessionStorage. SetItem (' object ', JSON.stringify({})) expect(session.get('object')).to.deep.equal({}); }); It (' get session', () => {expect(session. Get ('aaa')).to.be. }); }); })Copy the code
- Chestnut to set title
/** * set TAB page title * @param title */ export function setTitle(title) { And the address of the page contains "fragment identifiers" (that is, the url # after text) IE tabs title is automatically modified to url fragment identifiers if (userAgent (). The app = = = the MSIE | | userAgent (). The app = = = the Edge) { setTimeout(function () { document.title = title }, 1000)} else {document. The title = title}} / * write write write write function being measured write write write write * / / * left left left left test code left left left left down down down down down down down * / it (' Settings TAB page title# setTitle ', Const originalTitle = document.title; () => {// This test is not a good test, the test code changes the page DOM structure const originalTitle = document.title; // Save the original title and restore the const clock = sinon.usefaketimers () setTitle('test title'); clock.tick(1000) expect(document.title).to.equal('test title') setTitle(originalTitle); Tick (1000) clock.restore()})Copy the code
React component unit test
The Test of the React component is essentially similar to other unit tests, but the unit test of the React component is much more complex due to the “browser rendering” problem. Besides the basic UI components (such as Button and Link), It is difficult to keep the “size” within the “minimum test unit” range, although it is possible to reduce the complexity of components by “shallow rendering” (not rendering child components). From the perspective of the scope and significance of unit testing, in fact, most UI component testing is difficult to be included in unit testing. Because “everyone seems to take it for granted,” this document still describes UI component testing as unit testing.
Without opening the box, we couldn’t decide whether the cat was still alive or not.
React is not a black box, so to test React, you must first understand how React is rendered. Of course, we don’t need to know the React source code.
Briefly, a React component renders to a page in the following steps.
- The virtual DOM object is computed based on the state
- Generate a real DOM structure from the virtual DOM
- Mount the Dom to the page
But for our test, we don’t need to mount components to the page, just have a Dom environment, or even no Dom environment if you don’t need a full rendering component.
The Enzyme is Airbnb’s React test tool, Provides a Shallow Rendering render (Shallow) | Full Rendering (Full Rendering) | Static Rendering render (Static) several ways to test the React to Rendering component, usually used light Rendering method for testing.
Initialize the React adapter before using the Enzyme test. For details, see the official manual
-
Shallow Rendering
Only the outermost structure of the component is rendered; no child components are rendered
Example of a complete Shallow rendering test:
import * as React from 'react' import * as classnames from 'classnames' import { ClassName } from '.. /helper' import * as styles from './styles.desktop.css' const AppBar: React.StatelessComponent<any> = function AppBar({ className, children, ... props } = {}) { return ( <div className={classnames(styles['app-bar'], ClassName.BorderTopColor, className)} {... props}>{children}</div> ) } export default AppBarCopy the code
import * as React from 'react'; import { shallow } from 'enzyme'; import { expect } from 'chai'; import AppBar from '.. / UI. Desktop 'the describe (' ShareWebUI' () = > {the describe (' AppBar @ desktop, () = > {the describe (' # render '() = > {it (' default rendering, () => { shallow(<AppBar></AppBar>) }); It (' render child nodes correctly ', () => { const wrapper = shallow(<AppBar><div>test</div></AppBar>) expect(wrapper.contains(<div>test</div>)).to.be.true }); It (' allow setting ClassName', () => { const wrapper = shallow(<AppBar className='test'></AppBar>) expect(wrapper.hasClass('test')).to.be.true }); It (' allow setting other custom props', () => { const wrapper = shallow(<AppBar name='name' age={123}></AppBar>) expect(wrapper.props()).to.be.include({ name: 'name', age: 123 }) }); }); }); });Copy the code
-
Full Rendering
Render the actual Dom structure of the component, which you must use if you want to test native events.
-
Static Rendering
Just like what the crawler gets, gets the HTML string of the page, and uses the Cheerio to do that.
The event simulation in Enzyme’s shallow render mode is not a real event trigger, it is actually a “smokescreen” implementation, such as ButtonWrapper.Simulate (‘ click ‘) which simply calls the function that passes the onClick parameter to the Button component.
Airbnb. IO /enzyme/docs…
Many pits for Enzyme: airbnb. IO/Enzyme /docs…
If setState() is called in an asynchronous operation, then the test needs to assert in the next clock cycle (there are many similar problems) :
Find ('UIIcon'). Simulate ('click') > expect(wrapper.state('loadStatus')).to.equal(1) // Click immediately to change the loading state to loading > /* Set setState in promise.then, so need to assert on the next clock */ > setTimeout(() => {> expect(wrapper.state('loadStatus')).to.equal(2) > done() > }, 0) >Copy the code
Integration Testing
Integration test refers to the encapsulation of high-level functions or classes exposed by combination integration of tested unit test functions on the basis of unit test.
The biggest difficulty of integration test is that the granularity is larger, the logic is more complex, and there are more external factors, which cannot guarantee the control and independence of the test. The solution is to use a test peg (test surrogate), which is to replace the subfunction or module that is called, that hides the details of the submodule and controls its behavior to achieve the desired test. (The premise here is that the submodule has been fully covered by unit tests, so it can be assumed that the submodule status is knowable.)
Typescript compiles to Commonjs:
Import * as B from 'B' import {fn} from 'C' export function A() {b.fn.name ()} export class A1 {} // Define (["require", "exports", "B", "C"], function (require, exports, B, C_1) {"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); function A() { B.fn.name(); C_1.fn.name(); } exports.A = A; var A1 = /** @class */ (function () { function A1() { } return A1; } ()); exports.A1 = A1; });Copy the code
ExportName (exportName, exportName, exportName, exportName, exportName, exportName, exportName, exportName, exportName, exportName, exportName, exportName). Stub (obj,’fnName’). Stub (obj,’fnName’). In ES6, you can export the entire module to a single object by importing * as moduleName from ‘moduleName’ to resolve the Stub problem.
Consider having the following module dependencies:
Module A depends on module B and C, and module B depends on module C. In this case, you can choose only Stub module C. In this case, module C in module B will also be affected by the Stub. The best way is to Stub module B and module C at the same time.
Chestnuts with fried eggs
import { clientAPI } from '.. /.. /.. /clientapi/clientapi'; /** * Obtain cache information from relative path * @param param0 Parameter object * @param param0.relPath Relative path */ export const getInfoByPath: Core.APIs.Client.Cache.GetInfoByPath = function ({ relPath }) { return clientAPI('cache', 'GetInfoByPath', { relPath }); } /* Test function: Function under test: */ /* ↓↓↓↓ Test code ↓↓↓↓ */ import {expect} from 'chai' import {stub} from 'sinon' import * as clientAPI from '.. /.. /.. /clientapi/clientapi'; import { getInfoByPath, getUnsyncLog, getUnsyncLogNum } from './cache' describe('cache', () => { beforeEach('stub', () => { stub(clientapi, 'clientAPI') }) afterEach('restore', () = > {clientapi. Clientapi. Restore ()}) it (' # cache information captured by the relative path getInfoByPath, () = > {getInfoByPath ({relPath: 'relPath'}) expect(clientapi.clientapi.args [0][0]).to.equal('cache') // Request resource correct Expect (clientapi.clientapi.args [0][1]).to.equal('GetInfoByPath') // Request method is correct Expect (clientapi.clientapi.args [0][2]).to.deep.equal({relPath: 'relPath'})})Copy the code
Or use the Sandbox provided by Sinon, which is simpler when restoring and does not need to separately restore each stubbed object.
import { rsaEncrypt } from '.. /.. /util/rsa/rsa'; import { getNew} from '.. /apis/eachttp/auth1/auth1'; /** * authentication user * @param account {string} * @param password {string} */ export function auth(Account: string, password: string, ostype: number, vcodeinfo? : Core.APIs.EACHTTP.Auth1.VcodeInfo): Promise<Core.APIs.EACHTTP.AuthInfo> { return getNew({ account, password: encrypt(password), deviceinfo: { ostype: ostype }, vcodeinfo }); Measured function} / * write write write write write write write write * / / * left left left left test code left left left left down down down down down down down * / import {createSandbox} from 'sinon import * as auth1 from'.. /apis/eachttp/auth1/auth1' import * as rsa from '.. /.. /util/rsa/rsa' const sandbox = createSandbox() describe('auth', () => { beforeEach('stub',()=>{ sandbox.stub(rsa,'rsaEncrypt') sandbox.stub(auth1,'getNew') }) afterEach('restore',()=>{ Sandbox. Restore ()}) it(' authentication user #auth', () => {auth('account', 'password', 1, {uuid: '12140661-e35b-4551-84cf-ce0e513d1596', vcode: '1abc', ismodif: False}) rsa. RsaEncrypt. Returns (' 123 ') / / control the return value expect (rsa) rsaEncrypt) calledWith (" password ")). To.. Be true expect(auth1.getNew.calledWith({ account: 'account', password: '123', deviceinfo: { ostype: 1 }, vcodeinfo: { uuid: '12140661-e35b-4551-84cf-ce0e513d1596', vcode: '1abc', ismodif: false } })).to.be.true }) }Copy the code
Reference Documents:
- Related concepts and use of stubs-sinon.js Stubs
- Sandboxes-sinon.js Concepts and use of Sandboxes
E2E Testing
End-to-end testing is the ultimate level of testing, where you treat the application as a complete black box as a user, open the application to simulate input, and check whether the functionality and interface are correct.
Some issues that need to be addressed in end-to-end testing:
-
Environmental problems
That is, how to ensure that the environment is “clean” before each test, for example, you need to check that the list is empty. If the list is added in the last test, the next test will not get the empty list state.
The simplest solution is to call an external script to clean up the database before or after all the tests, or you can intercept the request and customize the response (which makes the tests more complex and less “real”).
-
Elements to find
If the code is constantly changing, the component structure is constantly changing, and if you look for action elements based on the DOM structure, you will be stuck in maintenance selector hell. The best practice is to use the test-ID approach, but this approach requires the developer and tester to work together to define semantic test-IDS on operational elements.
-
Waiting for operation
Such as an interface change due to an asynchronous network request, or an interface animation, the timing of retrieving operation elements is unknown. The solution waits until the listening request completes and the desired element is successfully fetched.
-
Use actions instead of assertions
You should rely more on operations than assertions. For example, if an operation depend on the elements of A presence, you don’t need to “determine whether elements in the page A existence”, and should go directly to obtain “element A, and operation”, because if the element does not exist, then certainly will get less than, assert that after operation is meaningless, so can be used directly to replace operation assertion wait for function.
Refer to the end-to-end testing documentation