如何开始测试,怎么写好测试?

如何开始测试,怎么写好测试?

十一月 21, 2020

怎么开始测试呢?

编写测试之前,请先深呼吸气沉丹田,想想自己即将要攀登到代码质量之巅

感受着和煦的微风微凉的空调风夹杂着氟利昂的气息,抚过脸颊,默默拿起手边的Mojito温热的白开水伴随着野菊花的芬芳

默念心法given-when-then不要去关注内部逻辑怎么实现的

缓缓开始敲击着键盘:
describe(xxx, () => {})……


正经开始吧,先从TDD测试模式开始起步吧。

  • 这里我们接了一个需求,需要写一个过滤出URL地址中的端口号工具函数。

首先我们可能会有这样一个敏捷的思维,这个工具函数就是1. 拿到 url => 2. 匹配 端口号 => 3. 返回端口号

于是这样一个用例出来了。这里按照预期测试完,确实返回了8080这个端口号

1
2
3
4
5
6
7
8
9
10
11
import { filterPort } from 'utils';
import assert from 'assert';

describe('Util', (): void => {
test('返回端口号', (): void => {
const url: string = 'http://localhost:8080';
const res = filterPort(url);

assert(res === '8080');
})
});

image

按照TDD开发的顺序,先跟着feel盲写一波测试用例,然后开始简要的开发,让测试用例通过。

1
2
3
4
// 定义函数模板
export interface FilterPortProps {
(url: string): string | undefined;
}
1
2
3
4
5
6
// 编写最基本的函数
import { FilterPortProps } from '../lib/interface/utils';

export const filterPort: FilterPortProps = (url) => {
return url.match(/(?<=:)\d+/g)?.[0];
}

接下来就是紧张而又自豪的时刻了:

image

按照TDD的规则,我们继续编写测试(未实现的测试);

1
2
3
4
5
6
test('返回 null', (): void => {
const url: string = 'http://localhost';
const res = filterPort(url);
console.log(res)
assert(res === null);
});

image

这里的行为,应该是返回null,所以源代码应该修改为这样:

1
2
3
4
5
import { FilterPortProps } from '../lib/interface/utils';

export const filterPort: FilterPortProps = (url) => {
return url.match(/(?<=:)\d+/g)?.[0] || null;
}

image

我们再次完成了一个 由绿的过程,但是这时候的设计并不完美。

再从BDD深扣实现细节出发,返回出一个正确的结果,我们需要两个步骤:

  1. url确实存在;
  2. 正则匹配返回结果;

我们思考的是,在什么情况下,会有什么表现,代码层面会有什么体现。

所以针对这个工具函数,我们应该遵循黑盒测试用例设计方案,设计有效/无效等价类;

  • 有效等价类

    1. url存在,且只有一个
  • 无效等价类

    1. url 不存在
    2. url 存在,但是数目大于一个

场景一:在后端数据存在问题时,我们的程序应该爆出合适的错误去引导开发者最快速度的定位到错误;

场景二:在别的开发者使用错误的时候,但是并不阻碍程序正常运行,应该适当地爆出warning去引导且告诉使用者,这样做是违反我工具的使用规则的;

接下来,我们的测试用例又会新增两条用例去覆盖我们的无效等价类或者BDD场景

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 源代码
import { FilterPortProps } from '../lib/interface/utils';

export const filterPort: FilterPortProps = function (url) {
// if url is undefined \ null \ ''
if(!url) { throw new Error('Function expect a param at least'); }

// if arguements's length 大于 1
if(arguments.length > 1) {
console.warn(`
Function only handle one param, if you want to handle one group params:
you can use example as follow:
[param1, param2, ...].reduce(a, b => a.concat(filterPort(item)), [])
`)
}

return url.match(/(?<=:)\d+/g)?.[0] || null;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
import { filterPort } from '../../../utils';
import assert from 'assert';

describe('Util', (): void => {

it('当调用工具函数后,且传入正确的 url 并且匹配到正确结果', (): void => {
const url: string = 'http://localhost:8080';
const res = filterPort(url);

assert(res === '8080');
});

it('当调用工具函数后,且传入正确的 url,但没有匹配到结果应该返回null', (): void => {
const url: string = 'http://localhost';
const res = filterPort(url);

assert(res === null);
});

it('当调用工具函数后,接收入参为null的情况下,程序应该反馈出正确的信息', (): void => {
const url = '';
try {
filterPort(url);
} catch (e) {
assert(e.toString().length > 1);
}
});

it('当调用工具函数后,接收参数超过一个的时候,只匹配第一个参数的结果,并且程序应该给出warning提示', (): void => {
const url: string = 'http://localhost:8080';
global.console.warn = jest.fn();
const res = filterPort(url, url);
assert(res === '8080');
expect(global.console.warn).toBeCalledTimes(1);
});
});

最后:

image

image

相信你已经对于如何去设计一个完美的测试用例有了一个大体的认识,也了解了从UTDDATDD层级去驱动设计测试进而驱动开发。最后,始终去关注测试原则:
保证given-when-then细则;

只考虑

名词解释:
ATDD(“Acceptance Test Driven Development): 验收测试驱动,所有的产品(代码产出)都应该符合验收细则,而不是虚拟的指标;(应该出现的时间节点在 需求分析时)
UTDD(Unit Test Driven Development): 单元测试驱动开发;(应该出现的时间节点在于:代码开发之前)

然后可以聊聊重构?

什么是重构,重构是重构代码细节,但是重构后的应用的表现形态是不该不破坏的。

依旧拿上面的示例来讲,有一天我接到了一个需求,要重构上面的函数,实现方案是不允许用正则去处理:

A同学,使用了一些奇淫巧技实现了这个功能,完了之后去跑我们的测试用例;用例应该是正常运行的。

这样的测试用例才算是健壮的,利于重构代码的,可以为重构代码提供导向性作用;

可能你对于上面的表述会显得有些意识模糊

那么我们开始一个简单的 todo list 开发;

需求:请完成一个todolist,纯新增:支持回车新增与按钮点击新增;

从(UTDD)角度出发:

分三个组件:

  • Operate-Panel用于操作;
  • List用于展示todo item组;
operate-panel

先大致建好文件,分析测试:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
describe('Operate-Panel', (): void => {

let wrapper: ShallowWrapper;
const dispatch = jest.fn();

beforeEach(() => {
wrapper = shallow(<OperatePanel dispatch={dispatch}/>)
});
afterEach(() => {
wrapper.unmount();
jest.clearAllMocks();
});

test('输入内容后,回车调用外层的 dispatch', () => {
const todoContent = '今天需要读书';
wrapper.find('input').simulate('input', { target: { value: todoContent } });
wrapper.find('input').simulate('keyup', { key: 'Enter' });
expect(dispatch).toBeCalledTimes(1);
// expect(dispatch).toHaveBeenCalledWith(todoContent);
});

test('input 内容为空时,什么都不做,不触发 dispatch', () => {
const todoContent = '今天需要读书';
wrapper.find('input').simulate('input', { target: { value: todoContent } });
expect(dispatch).toBeCalledTimes(0);
});

test('输入内容后,不回车,不会触发 dispatch', () => {
const todoContent = '今天需要读书';
wrapper.find('input').simulate('input', { target: { value: todoContent } });
wrapper.find('input').simulate('keyup', { key: 'Tab' });
expect(dispatch).toBeCalledTimes(0);
});

});

image

然后按照这个思路完善与这个测试相关的业务代码,不要多写与当前测试用例无关的业务代码;

根据TDD测试模式,测试覆盖率通常可以达到90%-100%之间。
(小tip:在进行测试用例的设计中,可以依照白盒测试设计:路径覆盖、逻辑覆盖)
image

那么其实与TDD相互配合的还有BDD测试模式

BDD的行为细则属于怎样的呢?

全称:Behavior Driven Development行为驱动开发。说的直白一点,更多的应该是,需求驱动开发,也就是当前组件的测试是面向需求的,需求满足则测试通过。
但是不好的一点是失去了单测目的,非需求部分的逻辑代码得不到测试覆盖,无法谈及质量观。

测试模式宏观的方案就是写完业务代码后,针对需求进行编撰测试。