What is a test
Wikipedia definition:
The process of operating a program under specified conditions in order to detect errors, measure software quality, and evaluate whether it meets design requirements.
The purpose of testing is to improve code quality and maintainability.
- Improve code quality: Testing is finding bugs, finding bugs, and fixing them. Fewer bugs means better code quality.
- Maintainability: The lower the cost of modifying and adding functionality to existing code, the higher the maintainability.
When are tests written
If your program is very simple, you don’t need to write tests. For example, the following program, simple function, only a dozen lines of code:
function add(a, b) {
return a + b
}
function sum(data = []) {
let result = 0
data.forEach(val= > {
result = add(result, val)
})
return result
}
console.log(sum([1.2.3.4.5.6.7.8.9.10])) / / 55
Copy the code
If you have a program that has hundreds of lines of code, but it’s well packaged, it’s a perfect example of modularity. Each module has a single function, less code, and you don’t need to write tests.
If your program has tens of thousands of lines of code and dozens of modules, the interactions between modules are intricate. In that case, you need to write tests. Imagine if you made changes to a very complex project and there were no tests. You need to manually test every feature associated with this change to prevent bugs. But if you write a test, you only need to execute a command to know the result, saving time and effort.
Test types and frameworks
There are many types of testing: unit testing, integration testing, white box testing…
There are also a variety of testing frameworks: Jest, Jasmine, LambdaTest…
This chapter will only cover unit testing and E2E end-to-end testing. The testing framework used for unit tests is Jest, and the testing framework used for E2E is Cypress.
Jest
The installation
npm i -D jest
Copy the code
Open the package.json file and add the test command under scripts:
"scripts": {
"test": "jest",}Copy the code
Then create a new test directory under the project root directory as the test directory.
Unit testing
What are unit tests? Wikipedia defines it as:
Unit Testing (English: Unit Testing), also known as module Testing, is a test for the correctness of the program module (the smallest Unit of software design).
From a front-end perspective, unit testing is a test of a function, a component, or a class, which is relatively granular.
How should unit tests be written?
- Write tests for correctness, that is, correct input should have normal results.
- Write tests for error, meaning that bad input should be bad results.
Test a function
For example, if a function abs() takes absolute values, input 1,2, the result should be the same as the input; Negative 1, negative 2, the result should be the opposite of the input. If the input is non-numeric, such as “ABC”, a type error should be thrown.
// main.js
function abs(a) {
if (typeofa ! ='number') {
throw new TypeError('Parameter must be numeric')}if (a < 0) return -a
return a
}
// test.spec.js
test('abs'.() = > {
expect(abs(1)).toBe(1)
expect(abs(0)).toBe(0)
expect(abs(-1)).toBe(1)
expect(() = > abs('abc')).toThrow(TypeError) // Type error
})
Copy the code
Now we need to test the abs() function: create a main.js file in the SRC directory and a test.spec.js file in the test directory. Then write the above two function code to the corresponding file, execute the NPM run test, to see the effect of the test.
Test a class
Suppose we have a class like this:
class Math {
abs(){}sqrt(){}pow(){}... }Copy the code
We must test all the methods of this class.
test('Math.abs'.() = > {
// ...
})
test('Math.sqrt'.() = > {
// ...
})
test('Math.pow'.() = > {
// ...
})
Copy the code
Test a component
Component testing is difficult because many components involve DOM manipulation.
For example, an upload image component has a method that converts the image to base64 code. How do you test that? Most tests run in a Node environment, which has no DOM objects.
Let’s review the process of uploading images:
- Click on the
<input type="file" />
, select Upload picture. - The trigger
input
的change
Event, getfile
Object. - with
FileReader
Convert the image to base64 code.
This process is the same as the following code:
document.querySelector('input').onchange = function fileChangeHandler(e) {
const file = e.target.files[0]
const reader = new FileReader()
reader.onload = (res) = > {
const fileResult = res.target.result
console.log(fileResult) // Output base64 code
}
reader.readAsDataURL(file)
}
Copy the code
The above code is just a simulation, but in real life it would look like this:
document.querySelector('input').onchange = function fileChangeHandler(e) {
const file = e.target.files[0]
tobase64(file)
}
function tobase64(file) {
return new Promise((resolve, reject) = > {
const reader = new FileReader()
reader.onload = (res) = > {
const fileResult = res.target.result
resolve(fileResult) // Output base64 code
}
reader.readAsDataURL(file)
})
}
Copy the code
As you can see, the window’s event object, FileReader, appears in the code above. That is, as long as we can provide these two objects, we can run it in any environment. So we can add these two objects to our test environment:
/ / rewrite the File
window.File = function () {}
/ / rewrite FileReader
window.FileReader = function () {
this.readAsDataURL = function () {
this.onload
&& this.onload({
target: {
result: fileData,
},
})
}
}
Copy the code
The test can then be written like this:
// Write the document in advance
const fileData = 'data:image/test'
// Provide a false file object to the tobase64() function
function test() {
const file = new File()
const event = { target: { files: [file] } }
file.type = 'image/png'
file.name = 'test.png'
file.size = 1024
it('file content'.(done) = > {
tobase64(file).then(base64= > {
expect(base64).toEqual(fileData) // 'data:image/test'
done()
})
})
}
// Execute the test
test()
Copy the code
By doing this hack, we can test components that involve DOM manipulation. My vue-upload-imgs library writes unit tests in this way, if you’re interested (test files are in the test directory).
Test coverage
What is test coverage? It is expressed by a formula: Code coverage = number of code executed/total number of code. Jest To enable test coverage statistics, simply add the –coverage parameter to the Jest command:
"scripts": {
"test": "jest --coverage",}Copy the code
Now let’s try it again with the previous test case to see the test coverage.
// main.js
function abs(a) {
if (typeofa ! ='number') {
throw new TypeError('Parameter must be numeric')}if (a < 0) return -a
return a
}
// test.spec.js
test('abs'.() = > {
expect(abs(1)).toBe(1)
expect(abs(0)).toBe(0)
expect(abs(-1)).toBe(1)
expect(() = > abs('abc')).toThrow(TypeError) // Type error
})
Copy the code
The figure above shows that each item has 100% coverage.
Now let’s comment out the line with the wrong test type and try again.
// test.spec.js
test('abs'.() = > {
expect(abs(1)).toBe(1)
expect(abs(0)).toBe(0)
expect(abs(-1)).toBe(1)
// expect(() => abs('abc')).toThrow(TypeError)
})
Copy the code
You can see that test coverage is down. Why is that? Because the abs() branch of the function that determines the type error is not executed.
// This is the branch statement
if (typeofa ! ='number') {
throw new TypeError('Parameter must be numeric')}Copy the code
Coverage statistics
It can be seen from the picture of coverage rate that there are four statistical items in total:
- Stmts(Statements) : Statement coverage, whether each statement in a program has been executed.
- Branch: Branch coverage, whether each Branch is executed.
- Funcs: Function coverage, whether or not each function is executed.
- Lines: Line coverage, whether each line of code is executed.
Some of you might wonder, isn’t 1 the same as 4? No, because one line of code can contain several statements.
if (typeofa ! ='number') {
throw new TypeError('Parameter must be numeric')}if (typeofa ! ='number') throw new TypeError('Parameter must be numeric')
Copy the code
For example, the two pieces of code above have different test coverage. Now comment out the line with the wrong test type and try again:
// expect(() => abs('abc')).toThrow(TypeError)
Copy the code
Coverage for the first code:
Coverage for the second code:
They all have the same unexecuted statements, but the first line of code has lower Lines coverage because it has one unexecuted line. The unexecuted statement in the second piece of code is on the same line as the judgment statement, so Lines coverage is 100%.
TDD test-driven development
Test-driven Development (TDD) is to write Test code in advance based on requirements, and then implement functions based on the Test code.
TDD’s intentions are good, but if your needs are constantly changing (you get the idea), that’s not a good thing. Chances are, you’re changing your test code every day, and your business code doesn’t change much.
So TDD also depends on whether the business requirements change frequently and whether you have a clear understanding of the requirements.
E2E test
End-to-end testing, which simulates the user performing a series of actions on a page and verifies that it meets expectations. This chapter explains E2E testing using Cypress.
When Cypress tests E2E, it opens the Chrome browser and acts on the page according to the test code, just like a normal user would.
The installation
npm i -D cypress
Copy the code
Open the package.json file and add a command to scripts:
"cypress": "cypress open"
Copy the code
Then run NPM run cypress to open cypress. Opening it for the first time automatically creates the default test script provided by Cypress.
Click Run 19 Integration Specs on the right to start the tests.
First test
Open the Cypress directory and create a new e2e.spec.js test file in the Integration directory:
describe('The Home Page'.() = > {
it('successfully loads'.() = > {
cy.visit('http://localhost:8080')})})Copy the code
Run it, and you should see a test failure if there is no accident.
Because the test file requires access to the http://localhost:8080 server, which is not available yet. So we need to create a server using Express, create a new server.js file, and enter the following code:
// server.js
const express = require('express')
const app = express()
const port = 8080
app.get('/'.(req, res) = > {
res.send('Hello World! ')
})
app.listen(port, () = > {
console.log(`Example app listening at http://localhost:${port}`)})Copy the code
Execute Node Server.js, rerun the test, and this time you should see the correct results.
PS: If you use ESlint to validate code, you need to download the eslint-plugin-cypress plugin, otherwise the global cypress command will report an error. After downloading the plug-in, open the.eslintrc file and add cypress to the plugins option:
"plugins": [
"cypress"
]
Copy the code
Impersonating user Login
The last test was a bit childish, so let’s write a slightly more complex test that mimics a user login:
- The user opens the login page
/login.html
- Enter the account password (both
admin
) - After the login is successful, go to
/index.html
First we need to rewrite the server and modify the code in the server.js file:
// server.js
const bodyParser = require('body-parser')
const express = require('express')
const app = express()
const port = 8080
app.use(express.static('public'))
app.use(bodyParser.urlencoded({ extended: false }))
app.use(bodyParser.json())
app.post('/login'.(req, res) = > {
const { account, password } = req.body
// Since there is no registration function, it is assumed that the account password is admin
if (account == 'admin' && password == 'admin') {
res.send({
msg: 'Login successful'.code: 0})},else {
res.send({
msg: 'Login failed, please enter the correct account password'.code: 1,
})
}
})
app.listen(port, () = > {
console.log(`Example app listening at http://localhost:${port}`)})Copy the code
Since there is no registration function, the password of the user is admin. Then create two NEW HTML files: login. HTML and index.html, and place them in the public directory.
<! -- login.html -->
<! DOCTYPEhtml>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="Width = device - width, initial - scale = 1.0">
<title>login</title>
<style>
div {
text-align: center;
}
button {
display: inline-block;
line-height: 1;
white-space: nowrap;
cursor: pointer;
text-align: center;
box-sizing: border-box;
outline: none;
margin: 0;
transition: 0.1 s;
font-weight: 500;
padding: 12px 20px;
font-size: 14px;
border-radius: 4px;
color: #fff;
background-color: #409eff;
border-color: #409eff;
border: 0;
}
button:active {
background: #3a8ee6;
border-color: #3a8ee6;
color: #fff;
}
input {
display: block;
margin: auto;
margin-bottom: 10px;
-webkit-appearance: none;
background-color: #fff;
background-image: none;
border-radius: 4px;
border: 1px solid #dcdfe6;
box-sizing: border-box;
color: # 606266;
font-size: inherit;
height: 40px;
line-height: 40px;
outline: none;
padding: 0 15px;
transition: border-color 0.2 s cubic-bezier(0.645.0.045.0.355.1);
}
</style>
</head>
<body>
<div>
<input type="text" placeholder="Please enter your account number" class="account">
<input type="password" placeholder="Please enter your password" class="password">
<button>The login</button>
</div>
<script src="https://cdn.bootcdn.net/ajax/libs/axios/0.21.0/axios.min.js"></script>
<script>
document.querySelector('button').onclick = () = > {
axios.post('/login', {
account: document.querySelector('.account').value,
password: document.querySelector('.password').value,
})
.then(res= > {
if (res.data.code == 0) {
location.href = '/index.html'
} else {
alert(res.data.msg)
}
})
}
</script>
</body>
</html>
Copy the code
<! -- index.html -->
<! DOCTYPEhtml>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="Width = device - width, initial - scale = 1.0">
<title>index</title>
</head>
<body>
Hello World!
</body>
</html>
Copy the code
The login. Static HTML pages
The index.html static pages
Then change the contents of the test file:
describe('The Home Page'.() = > {
it('login'.() = > {
cy.visit('http://localhost:8080/login.html')
// Enter the account password
cy.get('.account').type('admin')
cy.get('.password').type('admin')
cy.get('button').click()
// Redirect to /index
cy.url().should('include'.'http://localhost:8080/index.html')
// Assert that the index. HTML page contains Hello World! The text
cy.get('body').should('contain'.'Hello World! ')})})Copy the code
Now re-run the node server.js server and run NPM run cypress. Click run… Start testing.
The test result is correct. To unify the usage of scripts, it is best to replace the node server.js command with NPM run start:
"scripts": {
"test": "jest --coverage test/"."lint": "eslint --ext .js test/ src/"."start": "node server.js"."cypress": "cypress open"
}
Copy the code
summary
All of the test cases in this chapter are available on my Github, so I suggest you clone the project and run it yourself.
The resources
- What exactly is a unit test? What should be done? – Coolhappy’s answer
- Jest
- Cypress
- Code coverage
Get you started with front-end engineering
- Technology selection: How to do the technology selection?
- Uniform specifications: How do you create specifications and use tools to ensure that they are strictly followed?
- Front-end componentization: What is modularization and componentization?
- Testing: How do I write unit tests and E2E (end-to-end) tests?
- Build tools: What are the build tools? What are the features and advantages?
- Automated Deployment: How to automate deployment projects with Jenkins, Github Actions?
- Front-end monitoring: explain the principle of front-end monitoring and how to use Sentry to monitor the project.
- Performance Optimization (I) : How to detect website performance? What are some useful performance tuning rules?
- Performance Optimization (2) : How to detect website performance? What are some useful performance tuning rules?
- Refactoring: Why do refactoring? What are the techniques for refactoring?
- Microservices: What are microservices? How to set up a microservice project?
- Severless: What is Severless? How to use Severless?