自动化测试概念
在开发中,测试无处不在,我们往往都是手动完成的。比如我要看一个按钮点击之后会不会出现对应效果,我们就会去点击这个按钮,然后看最终结果是否符合预期:
<button id="btn">点击</button>
<div id="content"></div>
<script>
const btnDom = document.querySelector('#btn')
const contentDom = document.querySelector('#content')
btnDom.addEventListener('click',() => {
contentDom.textContent='hello'
},false)
</script>
<script src="./yk-test.js"></script>
<script src="./test.js"></script>
<script src="./sum.js"></script>
<script src="./test-sum.js"></script>
自动化就是将这个过程 程序化,测试的过程由人工完成变为机器完成。那人工测试时的步骤要怎样程序化?
以点击按钮这一场景为例,我们的测试方式用语言描述,步骤如下:
- 获取按钮
- 点击按钮
- 获取按钮点击后影响结果的内容
- 拿预期结果与获取的结果进行比较,是否满足预期
以上描述用程序描述,伪代码:
//1.
const dom = document.querySelector('.btn')
//2.
dom.click()
const content = document.querySelector('content').textContent //1.+3.
//4.
if (content === 'clicked') {
console.log('PASS')
}
用测试代码来写:
//test.js
describe('UI button', () => { //describe() 用来描述你所需要测试的模块
//单个案例
it('button clicked', () => {
const dom = document.querySelector('.btn')
dom.click()
const content = document.querySelector('content').textContent
//需要用断言来进行判断 assert
expect(content).toBe('hello') //我期望的内容变成目标内容
})
})
简单封装一下这个用于测试的方法:
//yk-test.js
function d (name, callback) {
console.log(name);
callback()
}
function it(name, callback){
console.log(' '+name);
callback()
}
function expect(result) {
return {
toBe(exp) {
console.log(result === exp ? " PASS" : " FAIL",'expect:', exp,'get:',result)
}
}
}
//sum.js
function sum(a, b) {
return a + b;
}
//test-sum.js
describe('test math function', () => {
it('Adding 1 + 1 equals 2', () => {
expect(sum(1,1)).toBe(2)
expect(sum('1','1')).toBe(2) // 这个时候测试就会报错了
})
})
//sum.js
const isNumber = (num) => {
return Object.prototype.toString.call(num) === '[object Number]'
}
const sum = function(a, b) {
if (!isNumber(a) || !isNumber(b)) {
return null;
}
return a + b;
};
//test-sum.js
describe('test math function', () => {
it('Adding 1 + 1 equals 2', () => {
expect(sum(1,1)).toBe(2)
expect(sum('1', '1')).toBe(null); // 输入非数字返回 null
})
})
测试的价值与实现
为什么需要测试
测试对研发效率和管控的作用非常明显。好的测试,能够:
- 及时发现错误
- 拆分负责代码块,必要时写单测
- 集成到工程化构建工作流中
- 提高代码质量
测试类型
针对测试所涉及代码量与范围,通常可以将测试分为:
- 单元测试(Unit Test),可以理解为小的功能测试
- 集成测试(Integration Test),可以理解为小的模块测试
- UI 测试(UI Test),可以理解为从页面出发去测试整体功能(仅前端)
- E2E 测试(UI + 接口)
什么时候或场景考虑测试
不是所有项目都适合写测试,因为通常项目需求在早期都不一定稳定,需求开发时间短,这个时候如果贸然加入自动化测试,无疑增加了开发成本。
最适合引入自动化测试的场景为:
- 公共库一类的项目开发维护
- 中长期项目的迭代重构
- 需求趋于稳定后的系统
前端测试框架对比与实践
- 测试框架:Jest、@vue/test-utils、Mocha、Jesmine、Tape、Tyu、Ava
- 测试运行工具:Karam
- 断言库:Assert、Should、Chai
- 测试辅助工具:Sinon
- 测试覆盖率工具:Istanbul
- 无头浏览器:puppeteer
测试框架
测试框架通常选用 Jest,如果之前没有了解过任何测试框架的话,那么不用纠结直接先用上 Jest 再说。不过如果使用的是 Vue,那么推荐使用 @vue/test-utils
Jest
Jest 是一个基于 Node 的测试运行工具, 这意味着测试总是在 Node 环境中运行,而不是在真正的浏览器中。
Jest是一个“零配置”的前端测试工具,具有诸如模拟和代码覆盖之类开箱即用的特性,其广泛运用于 React、Vue 及其他 JavaScript 框架,是React 应用测试的第一选择。它具有:
- 强大的 Mock 功能
- 代码覆盖率
- 快照测试
@vue/test-utils
用于测试 Vue,如果是 Vue 应用,你也不爱折腾,那就直接选它好了
Mocha
它自身集成度不高,所以在使用时开发者需要根据自身喜好,选择相应运行库(例如 Karma)、断言库(例如 Chai)等组合使用。
Jest 在界面测试场景非常 Nice,但是在一些简单测试场景,Mocha 还是非常值得我们去使用的。比如:node 相关的测试、JavaScript 库相关的测试。
测试运行工具
常用的测试运行工具为:karma。
Karma 一般与 Mocha 结合使用,用于非 UI 类的测试。
karma-runner.github.io/6.4/index.h…
使用 Jest
Jest有一个非常强大的功能:快照测试。
先创建一个react项目:
npx create-react-app 项目名 --template typescript
快照测试
快照测试类似于“找不同”游戏。快照测试会给运行中的应用程序拍一张图片,并将其与以前保存的图片进行比较。如果图像不同,则测试失败。这种测试方法对确保应用程序代码变更后是否仍然可以正确渲染很有帮助
//Link.ts
import {useState} from 'react';
const STATUS = {
HOVERED: 'hovered',
NORMAL: 'normal',
};
export default function Link({page, children}) {
const [status, setStatus] = useState(STATUS.NORMAL);
const onMouseEnter = () => {
setStatus(STATUS.HOVERED);
};
const onMouseLeave = () => {
setStatus(STATUS.NORMAL);
};
return (
<a
className={status}
href={page || '#'}
onMouseEnter={onMouseEnter}
onMouseLeave={onMouseLeave}
>
{children}
</a>
);
}
使用React的测试渲染器和Jest的快照功能来与组件交互,并捕获渲染的输出创建一个快照文件:
Link.test.ts
import renderer from 'react-test-renderer';
import Link from '../Link';
it('changes the class when hovered', () => {
const component = renderer.create(
<Link page="http://www.facebook.com">Facebook</Link>,
);
let tree = component.toJSON();
expect(tree).toMatchSnapshot();
// manually trigger the callback
renderer.act(() => {
tree.props.onMouseEnter();
});
// re-rendering
tree = component.toJSON();
expect(tree).toMatchSnapshot();
// manually trigger the callback
renderer.act(() => {
tree.props.onMouseLeave();
});
// re-rendering
tree = component.toJSON();
expect(tree).toMatchSnapshot();
});
执行测试后,会得到类似这样的文件:
exports[`changes the class when hovered 1`] = `
<a
className="normal"
href="http://www.facebook.com"
onMouseEnter={[Function]}
onMouseLeave={[Function]}
>
Facebook
</a>
`;
exports[`changes the class when hovered 2`] = `
<a
className="hovered"
href="http://www.facebook.com"
onMouseEnter={[Function]}
onMouseLeave={[Function]}
>
Facebook
</a>
`;
exports[`changes the class when hovered 3`] = `
<a
className="normal"
href="http://www.facebook.com"
onMouseEnter={[Function]}
onMouseLeave={[Function]}
>
Facebook
</a>
`;
下次运行测试时,渲染的输出将与先前创建的快照进行比较。该快照应该与代码更改一起提交。当快照测试失败时,你需要检查是否为预期的或非预期的更改。如果更改是预期的,你可以使用jest -u来覆盖现有快照。
此示例的代码: examples/snapshot.
DOM 测试
如果想要断言和操作渲染的组件,可以使用react-testing-library或者Enzyme。
这里只演示 react-testing-library,Enzyme 类似,大家自行了解。
react-testing-library
pnpm add --save-dev @testing-library/react
我们编写一个 CheckBox 组件,其包含两个一个状态用于表示当前是否选中,并把结果渲染到页面。
CheckboxWithLabel.js
import {useState} from 'react';
export default function CheckboxWithLabel({labelOn, labelOff}) {
const [isChecked, setIsChecked] = useState(false);
const onChange = () => {
setIsChecked(!isChecked);
};
return (
<label>
<input type="checkbox" checked={isChecked} onChange={onChange} />
{isChecked ? labelOn : labelOff}
</label>
);
}
编写完我们在测试文件中编写测试用例代码
import {cleanup, fireEvent, render} from '@testing-library/react';
import CheckboxWithLabel from '../CheckboxWithLabel';
// Note: running cleanup afterEach is done automatically for you in @testing-library/react@9.0.0 or higher
// unmount and cleanup DOM after the test is finished.
afterEach(cleanup);
it('CheckboxWithLabel changes the text after click', () => {
const {queryByLabelText, getByLabelText} = render(
<CheckboxWithLabel labelOn="On" labelOff="Off" />,
);
expect(queryByLabelText(/off/i)).toBeTruthy();
fireEvent.click(getByLabelText(/off/i));
expect(queryByLabelText(/on/i)).toBeTruthy();
});
官方代码示例:examples/react-testing-library.
UI/E2E 测试的价值与实现
严格的 E2E 是包含数据请求的。
UI 测试通常需要借助无头浏览器来完成,一般使用 puppeteer
开发通常分为:本地、测试、线上;其实测试也分为三种模式
- 本地开发使用监听模式测试(--watch)
- 代码提交
- 流水线
具体思路为,使用无头浏览器加载页面,根据测试流程编写自动化测试代码以达到测试目的,会发现 UI Test 关注的是整体界面响应。
puppeteer:blog.logrocket.com/end-to-end-…
CyPress:blog.devgenius.io/writing-e2e…
CI/CD
实现 CI/CD 通常可以借助第三方平台,例如 git action、阿里云效、Jenkins、CircleCI 等。
打包构建以及一些开发过程中常用的操作需要自动化了,同样的,从打包到部署生产环境这个过程,也需要自动化,这个过程就是我们常说的 CI/CD。
CI/CD 是一种通过将自动化引入应用开发阶段来频繁向客户交付应用的方法。CI/CD 的主要概念是持续集成、持续交付和持续部署。CI/CD 是集成新代码可能给开发和运营团队带来的问题(又名“集成地狱”)的解决方案。
CI/CD 在应用的整个生命周期(从集成和测试阶段到交付和部署)中引入了持续的自动化和持续监视。
什么是持续集成 (CI)
持续集成是一种将所有代码更改尽早并经常集成到共享源代码存储库的主分支中的做法,在您提交或合并它们时自动测试每个更改,并自动启动构建。通过持续集成,可以更轻松地在开发过程中更早地识别和修复错误和安全问题。
通过频繁合并更改并触发自动测试和验证过程,可以最大限度地减少代码冲突的可能性,即使多个开发人员在同一应用程序上工作也是如此。第二个优点是不必等待很长时间才能得到答案,并且可以在必要时修复错误和安全问题,同时该主题在您的脑海中仍然记忆犹新。
常见的代码验证过程从验证代码质量的静态代码分析开始。一旦代码通过静态测试,自动化 CI 例程将打包并编译代码以进行进一步的自动化测试。 CI 流程应该有一个版本控制系统来跟踪更改,以便您知道所使用代码的版本。
什么是持续交付 (CD)
持续交付是一种软件开发实践,它与 CI 结合使用,可自动执行基础结构预配和应用程序发布过程。
作为 CI 过程的一部分测试和构建代码后,CD 会在最后阶段接管,以确保它包含随时部署到任何环境所需的一切。CD 可以涵盖从调配基础结构到将应用程序部署到测试或生产环境的所有内容。
使用 CD,软件的构建使其可以随时部署到生产环境。然后,可以手动触发部署或移动到持续部署,其中部署也是自动化的。
什么是持续部署
持续部署使组织能够自动部署其应用程序,无需人工干预。通过持续部署,团队会提前设置代码发布的条件,当满足并验证这些条件时,代码将部署到生产环境中。这使组织能够更加灵活,并更快地将新功能交到用户手中。
虽然可以在没有持续交付或部署的情况下进行持续集成,但如果没有 CI,则无法真正进行 CD。这是因为,如果不实践 CI 基础,例如将代码集成到共享存储库、自动执行测试和生成以及每天小批量完成所有操作,那么随时能够部署到生产环境将非常困难。
什么是持续测试
持续测试是一种软件测试实践,其中不断运行测试,以便在将错误引入代码库后立即识别错误。在 CI/CD 管道中,持续测试通常是自动执行的,每次代码更改都会触发一系列测试,以确保应用程序仍按预期运行。这有助于在开发过程的早期发现问题,并防止它们在以后变得更加困难和昂贵。持续测试还可以向开发人员提供有关其代码质量的宝贵反馈,帮助他们在将代码发布到生产环境之前识别和解决潜在问题。
在持续测试中,各种类型的测试在 CI/CD 管道中执行。这些可以包括:
- 单元测试,检查各个代码单元是否按预期工作
- 集成测试,验证应用程序中的不同模块或服务如何协同工作
- 回归测试,在修复错误后执行,以确保不会再次出现特定错误