背景
在敏捷开发场景中,产品迭代速度快、重构场景时有发生,如何进一步保证代码质量和系统稳定性?
一、测试基础理论
1. 测试框架
Jest
开箱即用 - 基本不需要额外的配置
功能强大 - 自带断言、测试覆盖率等工具,支持Mock、Snapshot和异步测试等
应用广泛 - 是vue cli和create-react-app默认集成的测试框架
2. Test Suit / Test Case
const myData = {
delicious: true,
sour: false,
};
// test suit,测试套件,表示一组相关用例的分组
describe("myData", () => {
// test case,测试用例,表示对一个功能点的测试
test("is delicious", () => {
expect(myData.delicious).toBeTruthy;
});
test("is not sour", () => {
expect(myData.sour).toBeFalsy;
});
});
3. 断言
Jest中的断言,称作“Matcher”
test("object assignment", () => {
const data = {
one: 1,
};
data["two"] = 2;
expect(data).toEqual({
one: 1,
two: 2,
});
});
test('null', () => {
const n = null;
expect(n).toBeNull();
expect(n).toBeDefined();
expect(n).not.toBeUndefined();
expect(n).not.toBeTruthy();
expect(n).toBeFalsy();
});
test('zero', () => {
const z = 0;
expect(z).not.toBeNull();
expect(z).toBeDefined();
expect(z).not.toBeUndefined();
expect(z).not.toBeTruthy();
expect(z).toBeFalsy();
});
4. 异步测试
promise和async / await场景下的异步测试
test("the data is Jack", () => {
return fetchData().then((data) => {
expect(data).toBe("Jack");
});
});
test("the data is Jack", async () => {
await expect(fetchData()).resolves.toBe("Jack");
});
5. Mock
一种覆盖原有函数、类的实际实现,来检测其调用情况的一种测试方法
// forEach.js
export function forEach(items, callback) {
for (const item of items) {
callback(item);
}
}
可以通过Mock函数的.mock属性,拿到其调用的各种信息
// forEach.test.js
const forEach = require('./forEach');
const mockCallback = jest.fn(x => 42 + x);
test('forEach mock function', () => {
forEach([0, 1], mockCallback);
// The mock function was called twice
expect(mockCallback.mock.calls).toHaveLength(2);
// The first argument of the first call to the function was 0
expect(mockCallback.mock.calls[0][0]).toBe(0);
// The first argument of the second call to the function was 1
expect(mockCallback.mock.calls[1][0]).toBe(1);
// The return value of the first call to the function was 42
expect(mockCallback.mock.results[0].value).toBe(42);
});
Mock一个第三方模块(目的:去除外部依赖,测试Users本身的逻辑)
// users.js
class Users {
static all() {
return axios.get('/users.json').then(resp => resp.data);
}
}
export default Users;
// users.test.js
import axios from 'axios';
import Users from './users';
jest.mock('axios');
test('should fetch users', () => {
const users = [{name: 'Bob'}];
const resp = {data: users};
axios.get.mockResolvedValue(resp);
// or you could use the following depending on your use case:
// axios.get.mockImplementation(() => Promise.resolve(resp))
return Users.all().then(data => expect(data).toEqual(users));
});
6. Snapshot
一种强大的测试工具,一般用于UI组件的测试
第一次运行用例:生成快照
第2 ~ n 次:使用快照对比现有组件
import renderer from 'react-test-renderer';
import Link from '../Link';
it('renders correctly', () => {
const tree = renderer
.create(<Link page="http://www.facebook.com">Facebook</Link>)
.toJSON();
expect(tree).toMatchSnapshot();
});
exports[`renders correctly 1`] = `
<a
className="normal"
href="http://www.facebook.com"
onMouseEnter={[Function]}
onMouseLeave={[Function]}
>
Facebook
</a>
`;
7. 测试覆盖率
Jest内置Istanbul模块,从以下4个维度统计测试覆盖率
- Istanbul在代码被执行之前,拦截了模块加载器,为其中的每一个逻辑分支、函数等添加了计数器,从而得到覆盖率结果。
- 实现原理:www.alloyteam.com/2019/07/134…
二、单元测试
- 只测试一个独立单元的工作
- 往往以组件、util为粒度
- 为什么要有单元测试?- 保证重构代码、版本迭代的安全性
1. 原则
AIR原则
- Automatic(自动化):不能有对人工操作的依赖
- Independent(独立性):用例不能有逻辑和顺序上的依赖关系
- Repeatable(可重复):尽可能不依赖外部环境
BCDE原则
- Border:边界值测试:循环边界、特殊取值、特殊长度、数据顺序等
- Correct:正确的输入和预期结果
- Design:与设计文档相结合,不能对着代码编写单元测试
- Error:强制错误信息输入(非法数据等),并得到预期结果
2. 工具
Enzyme - shallow / mount / render
Airbub出品,是对React官方测试工具库的封装
- Shallow Rendering(shallow):将React组件渲染成虚拟DOM,不渲染所有子组件
import { shallow } from 'enzyme'
import Foo from './Foo'
// 渲染速度极快
// wrapper对象中包含多种选择器
describe('MyComponet', () => {
it('renders thress <Foo /> components', ()=>{
const wrapper = shallow(<MyComponent />)
expect(wrapper.find(Foo)).to.have.lengthOf(3)
})
})
- Full Rendering(mount):将React组件渲染成真实DOM节点
import { mount } from 'enzyme'
import Foo from './Foo'
describe( '<Foo />' , ()=> {
it('allows us to set props', () => {
const wrapper = mount(<Foo bar="baz" />)
expect(wrapper.props().bar).to.equal( 'baz' )
wrapper.setProps({ bar: 'foo' })
expect(wrapper.props().bar ).to.equal('foo' )
})
})
- Static Rendering(render):将React组件渲染成静态HTML字符串
import { render } from 'enzyme'
import Foo from './Foo'
// warpper是Cheerio实例
describe('<Foo />' , ()=> {
it('renders a div', () => {
const wrapper = render(
<div className = 'myClass' />
)
expect(wrapper.html()).to.contain('div')
})
})
3. 案例
Ant Design - Switch
describe('Switch', () => {
// snapshot快照
it('should match snapshot', () => {
const wrapper = shallow(<Switch />);
expect(wrapper.render()).toMatchSnapshot();
});
// checked属性
it('should checked when padd checked=true props', () => {
const wrapper = render(<Switch checked={true}/>);
expect(wrapper.hasClass('ant-switch-checked')).toBe(true); // 找到对应的类名
});
// checkedChildren属性
it('should render checked children correctly', () => {
const wrapper = mount(<Switch checked={true} checkedChildren={'ok'}/>);
expect(wrapper.text()).toBe('ok');
});
// disabled属性
it('should be disable when pass props disable=true', () => {
const wrapper = render(<Switch disabled={true}/>);
expect(wrapper.hasClass('ant-switch-disabled')).toBe(true);
});
// onChange属性
it('should call onChange function when switch toggled', () => {
const change = jest.fn(checked => checked);
const wrapper = mount(<Switch onChange={change} />);
wrapper.simulate('click');
expect(change.mock.results[0].value).toBe(true);
wrapper.simulate('click');
expect(change.mock.results[1].value).toBe(false);
})
});
三、E2E测试
概念
- End to End(端到端)
- 模仿用户,从某个入口开始,逐步执行操作,直到完成某项工作
测试框架
Cypress - 一个轻量、现代、强大的E2E测试框架
- 支持快照: 可以查看每一步操作发生了什么
- 可调式性: 测试的过程中可以Debug
- 网络条件模拟: 模拟各种异常的网络状况
- 截图和录屏功能: 保留测试证据,方便分析测试结果
- 可视化: 有可视化的Dashboard方便用户操作
E2E测试示例 - ToDoList
describe('Main Process', () => {
beforeEach(() => {
cy.visit('http://localhost:8080');
});
it('Make a Todo', () => {
cy.get('input').first().type('some things');
cy.get('.v-input__append-outer .v-icon--link').click();
cy.get('.v-list>div .v-list__tile__content').first().should('have.text', 'some things');
});
it('Check a Todo', () => {
cy.get('input').first().type('some things');
cy.get('.v-input__append-outer .v-icon--link').click();
cy.get('.v-input--selection-controls__ripple').first().click();
cy.get('.v-list>div .v-list__tile__content .v-list__tile__title').first().should('have.class', 'done');
});
it('Deleta a Todo', () => {
cy.get('input').first().type('some things');
cy.get('.v-input__append-outer .v-icon--link').click();
cy.get('.v-list .v-list__tile__action .v-icon--link').first().click();
cy.wait(300);
cy.get('.v-btn__content').eq(1).click();
cy.get('.v-list>div').should('have.length', 0);
});
})
四、测试驱动开发
-
TDD(Test-Driven Development,测试驱动开发)
-
敏捷开发 - 快速迭代如何保证质量?
-
TDD原则
- 先编写完善的测试用例,把测试当作设计,再写代码
- 只允许编写刚好能够导致失败的单元测试
- 只允许编写刚好能够导致失败的单元测试【通过】的产品代码