覆盖率统计项
从覆盖率的图片可以看到一共有 4 个统计项:
- Stmts(statements):语句覆盖率,程序中的每个语句是否都已执行。
- Branch:分支覆盖率,是否执行了每个分支。
- Funcs:函数覆盖率,是否执行了每个函数。
- Lines:行覆盖率,是否执行了每一行代码。
可能有人会有疑问,1 和 4 不是一样吗?其实不一样,因为一行代码可以包含好几个语句。
if (typeof a != 'number') { throw new TypeError('参数必须为数值型') } if (typeof a != 'number') throw new TypeError('参数必须为数值型')
例如上面两段代码,它们对应的测试覆盖率就不一样。现在把测试类型错误的那一行代码注释掉,再试试:
// expect(() => abs('abc')).toThrow(TypeError)
第一段代码对应的覆盖率:
第二段代码对应的覆盖率:
它们未执行的语句都是一样,但第一段代码 Lines
覆盖率更低,因为它有一行代码没执行。而第二段代码未执行的语句和判断语句是在同一行,所以 Lines
覆盖率为 100%。
TDD 测试驱动开发
TDD(Test-Driven Development) 就是根据需求提前把测试代码写好,然后根据测试代码实现功能。
TDD 的初衷是好的,但如果你的需求经常变(你懂的),那就不是一件好事了。很有可能你天天都在改测试代码,业务代码反而没怎么动。
所以 TDD 用不用还得取决于业务需求是否经常变更,以及你对需求是否有清晰的认识。
E2E 测试
端到端测试,主要是模拟用户对页面进行一系列操作并验证其是否符合预期。本章将使用 Cypress 讲解 E2E 测试。
Cypress 在进行 E2E 测试时,会打开 Chrome 浏览器,然后根据测试代码对页面进行操作,就像一个正常的用户在操作页面一样。
安装
npm i -D cypress
打开 package.json
文件,在 scripts
新增一条命令:
"cypress": "cypress open"
然后执行 npm run cypress
就可以打开 Cypress。首次打开会自动创建 Cypress 提供的默认测试脚本。
点击右边的 Run 19 integration specs
就会开始执行测试。
第一次测试
打开 cypress
目录,在 integration
目录下新建一个 e2e.spec.js
测试文件:
describe('The Home Page', () => { it('successfully loads', () => { cy.visit('http://localhost:8080') }) })
运行它,如无意外应该会看到一个测试失败的提示。
因为测试文件要求访问 http://localhost:8080
服务器,但现在还没有。所以我们需要使用 express 创建一个服务器,新建 server.js
文件,输入以下代码:
// 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}`) })
执行 node server.js
,重新运行测试,这次就可以看到正确的结果了。
PS: 如果你使用了 ESlint 来校验代码,则需要下载 eslint-plugin-cypress
插件,否则 Cypress 的全局命令会报错。下载插件后,打开 .eslintrc
文件,在 plugins
选项中加上 cypress
:
"plugins": [ "cypress" ]
模仿用户登录
上一个测试实在是有点小儿科,这次我们来写一个稍微复杂一点的测试,模仿用户登录:
- 用户打开登录页
/login.html
- 输入账号密码(都是
admin
) - 登录成功后,跳转到
/index.html
首先需要重写服务器,修改一下 server.js
文件的代码:
// 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 // 由于没有注册功能,所以假定账号密码都为 admin if (account == 'admin' && password == 'admin') { res.send({ msg: '登录成功', code: 0, }) } else { res.send({ msg: '登录失败,请输入正确的账号密码', code: 1, }) } }) app.listen(port, () => { console.log(`Example app listening at http://localhost:${port}`) })
由于没有注册功能,所以暂时在后端写死账号密码为 admin
。然后新建两个 html 文件:login.html
和 index.html
,放在 public
目录。
<!-- login.html --> <!DOCTYPE html> <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.1s; 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.2s cubic-bezier(0.645, 0.045, 0.355, 1); } </style> </head> <body> <div> <input type="text" placeholder="请输入账号" class="account"> <input type="password" placeholder="请输入密码" class="password"> <button>登录</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> <!-- index.html --> <!DOCTYPE html> <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>
login.html 静态页
index.html 静态页
然后把测试文件内容改一下:
describe('The Home Page', () => { it('login', () => { cy.visit('http://localhost:8080/login.html') // 输入账号密码 cy.get('.account').type('admin') cy.get('.password').type('admin') cy.get('button').click() // 重定向到 /index cy.url().should('include', 'http://localhost:8080/index.html') // 断言 index.html 页面是包含 Hello World! 文本 cy.get('body').should('contain', 'Hello World!') }) })
现在重新运行服务器 node server.js
,再执行 npm run cypress
,点击右边的 Run...
开始测试。
测试结果正确。为了统一脚本的使用规范,最好将 node server.js
命令替换为 npm run start
:
"scripts": { "test": "jest --coverage test/", "lint": "eslint --ext .js test/ src/", "start": "node server.js", "cypress": "cypress open" }
小结
本章所有的测试用例都可以在我的 github 上找到,建议把项目克隆下来,亲自运行一遍。
参考资料
带你入门前端工程 全文目录:
- 技术选型:如何进行技术选型?
- 统一规范:如何制订规范并利用工具保证规范被严格执行?
- 前端组件化:什么是模块化、组件化?
- 测试:如何写单元测试和 E2E(端到端) 测试?
- 构建工具:构建工具有哪些?都有哪些功能和优势?
- 自动化部署:如何利用 Jenkins、Github Actions 自动化部署项目?
- 前端监控:讲解前端监控原理及如何利用 sentry 对项目实行监控。
- 性能优化(一):如何检测网站性能?有哪些实用的性能优化规则?
- 性能优化(二):如何检测网站性能?有哪些实用的性能优化规则?
- 重构:为什么做重构?重构有哪些手法?
- 微服务:微服务是什么?如何搭建微服务项目?
- Severless:Severless 是什么?如何使用 Severless?