测试策略


2018-12-1 test 单元测试 BDD TDD JEST

测试策略

敏捷项目下的

测试策略推荐在项目刚启动的时候制定 (产品需求范围,技术架构和交付计划大致确认的情况),指导项目的测试活动和方法。
讯息内容

  • 从Business了解到产品愿景,产品发展蓝图和业务流程 - 涉及需求分析和确定测试目标等活动
  • 从Project Manager了解到交付计划,交付范围 - 涉及确定测试类型、测试方法、测试阶段和测试重点等活动
  • 从Tech Lead了解到技术框架 - 涉及确定集成测试、自动化测试语言、工具和框架选型等活动

TEST.png

TDD(Test - Driven Development,测试驱动开发)

节奏是红 - 绿 - 重构,如下图 TEST.png

  • 思路:先写测试再开发程序,通过测试来推动整个开发的进行。
    思维:
    1. 单元测试的首要目的不是为了能够编写出大覆盖率的全部通过的测试代码,而是需要从使用者(调用者)的角度出发,尝试函数逻辑的各种可能性,进而辅助性增强代码质量。
    2. 测试是手段而不是目的。测试的主要目的不是证明代码正确,而是帮助发现错误,包括低级的错误。
    3. 测试要快。快速运行、快速编写。
    4. 测试代码保持简洁。
    5. 不会忽略失败的测试。一旦团队开始接受 1 个测试的构建失败,那么他们渐渐地适应 2、3、4 或者更多的失败。在这种情况下,测试集就不再起作用。别轻易放过失败。
  • 过程:
    1. 需求分析,思考实现。考虑如何“使用”产品代码,是一个实例方法还是一个类方法,是从构造函数传参还是从方法调用传参,方法的命名,返回值等。这时其实就是在做设计,而且设计以代码来体现。此时测试为红
    2. 实现代码让测试为”绿灯“
    3. 重构,然后重复测试

最终符合所有要求即:

  • 每个概念都被清晰的表达
  • 代码中无自我重复
  • 没有多余的东西
  • 通过测试
代码级测试

(人工静态方法) Peer review - 二次把关,发现错误。
(自动静态方法) ESLint、SonarLint - 主要的手段是代码静态扫描,可以发现语法特征错误、边界行为特征错误和经验特征错误这三类“有特征”的错误;
(人工动态方法) 单元测试 - 是发现算法错误和部分算法错误,这两类“无特征”的错误;

单元测试

原理:用驱动代码去调用被测函数,并根据代码的功能逻辑选择必要的输入数据的组合,然后验证执行被测函数后得到的结果是否符合预期。

什么是单元测试?

  • 单元测试为验证我们的代码是否可以按预期工作的手段。
  • 单元测试特指被测试对象为程序中最小组成单元的测试。最小组成单元可以是一个函数、一个类等等。

单元测试的意义

  1. TDD(测试驱动开发) 被证明是有效的软件编写原则,它能覆盖更多的功能接口。
  2. 快速反馈你的功能输出,验证你的想法。
  3. 保证代码重构的安全性,没有一成不变的代码,测试用例能给你多变的代码结构一个定心丸。
  4. 易于测试的代码,说明是一个好的设计。做单元测试之前,肯定要实例化一个东西,假如这个东西有很多依赖的话,这个测试建造过程将会非常耗时,会影响你的测试效率,怎么办呢?要依赖分离,一个类尽量保证功能单一,比如视图与功能分离,这样的话,代码也便于维护和理解。

单元测试的限制

  1. 占用开发人员的时间。但测试实属开发人员的工作一环,不应有多占时间的想法,只是现在回归正途。
  2. 单元测试覆盖率与代码质量没有必然的联系。更多的是工程师的素质提升。
  3. 般情况下,单元测试的代码量往往大于要测试的代码量,一般情况会是 1-2 倍

何时编写单元测试

  1. 开发过程中,单元测试应该来测试那些可能会出错的地方,或是那些边界情况。
  2. 维护过程中,单元测试应该围绕着 bug 进行,每个 bug 都应该编写响应的单元测试。从而保证同一个 bug 不会出现第二次。

单元测试过渡的作法与观念

  1. 优先对稳定的功能(比如一些公用组件)和核心流程编写单元测试。
  2. 如果项目里充斥着颗粒度低,方法间互相耦合的代码,会发现无法进行单元测试。要么重构已有代码,要么放弃单元测试寻求其他测试方法,比如人工测试,e2e 测试(後面篇幅有介紹)。
  3. 前端是一个非常复杂的测试环境,单元测试只能对功能每一个单元进行测试,对于一些依赖 api 的数据一般只能 mock,无法真正的模拟用户实际的使用场景。对于这种情况,建议采用其他测试方法,比如人工测试、e2e 测试。这也是为何我们要多做 e2e 测试。

以 Vue 开发的项目来说,我们要测试功能型组件、vue插件、二次封装的库。

单元测试思维

  1. 单元测试没有标准答案。
  2. 软件的质量不是测试出来的,而是设计和维护出来的。
  3. 粒度是多少,不重要,重要的是你软件应该怎么做,怎么测试。
  4. 先写测试,后写功能。

一个单元测框架框架含四部分

  • 测试运行器 Test Runner (可使用 edp-test, karma)
  • 测试框架 Testing Framework (可使用 jasmine, mocha, qunit, Jest)
  • 断言库 Assertion library: (可使用 expect.js, should, chai,)
  • 覆盖率 Coverage library: (可使用 istanbul)

如何写单元测试用例

  1. 测试代码时,只考虑测试,不考虑内部实现
  2. 数据尽量模拟现实,越靠近现实越好
  3. 充分考虑数据的边界条件
  4. 对重点、复杂、核心代码,重点测试
  5. 利用 AOP (beforeEach、afterEach), 减少测试代码数量,避免无用功能
  6. 测试、功能开发相结合,有利于设计和代码重构 单元测试 27 条准则

单元测试编写步骤

基本测试脚本
  • describe 是"测试套件"(test suite),表示一组相关的测试。它是一个函数,第一个参数是测试套件的名称("加法函数的测试"),第二个参数是一个实际执行的函数。
  • test 是"测试用例"(test case),表示一个单独的测试,是测试的最小单位。它也是一个函数,第一个参数是测试用例的名称,第二个参数是一个实际执行的函数。所有的测试用例(test 块)都应该含有一句或多句的断言。
describe('HelloWorld.vue', () => {
	test('should render correct contents', () => {
		// 断言库
		expect(content).to.equal('Welcome to Your Vue.js App')
	})
})
1
2
3
4
5
6
一组单元测试包含
  • 准备阶段:构造参数,创建 spy(监控对象) 等
  • 执行阶段:用构造好的参数执行被测试代码
  • 断言阶段:用实际得到的结果与期望的结果比较,以判断该测试是否正常
  • 清理阶段:清理准备阶段对外部环境的影响,移除在准备阶段创建的 spy 等
describe('Addition', () => {
    test('knows that 2 and 2 make 4', () => {
        const val1 = 2; // 准备阶段
        const val2 = 2; // 准备阶段
        const result = val1 + val2; // 执行阶段
        const expectedResult = 4;
        expect(result).toBe(expectedResult); // 断言阶段
        ....  // 清理阶段,如有需要清理,最后做清理动作
    });
});
1
2
3
4
5
6
7
8
9
10
单元测试的难点

单元测试无非就是用驱动代码去调用被测函数,并根据代码的功能逻辑选择必要的输入数据的组合,然后验证执行被测函数后得到的结果是否符合预期。但有以下难点

  1. 单元测试用例“输入参数”的复杂性;
    • 被测试函数的输入参数
    • 被测试函数内部需要读取的全局静态变量
    • 被测试函数内部需要读取的类成员变量
    • 函数内部调用子函数获得的数据
    • 函数内部调用子函数改写的数据
    • 嵌入式系统中,在中断调用中改写的数据
  2. 单+ 元测试用例“预期输出”的复杂性;
    • 被测函数的返回值
    • 被测函数的输出参数
    • 被测函数所改写的成员变量和全局变量
    • 被测函数中进行的文件更新、数据库更新、消息队列更新等
    • 关联依赖的代码不可用。

Jest 单元测试

Jest 集成了所需的测试功能,如

  • 可取代 Karma + Mocha + sinon chai + phantomjs + 一个用于测试的浏览器环境
  • 内置强大的断言与 mock 功能
  • 内置测试覆盖率统计功能
  • 内置 Snapshot 机制

安装

npm install jest --dev-save
or
yarn add jest --dev
1
2
3
{
    "scripts": {
        "test": "jest"
    }
}
1
2
3
4
5

文档架构

TEST.png

jest.conf.js 设定


const path = require('path')
 
 
module.exports = {
  verbose: true,  // 用于显示每个测试用例的通过与否
  rootDir: path.resolve(__dirname, '../../'),
  moduleFileExtensions: ['js', 'json', 'vue'],
  moduleNameMapper: {
    '\\.(css|styl|less|sass|scss)$': '<rootDir>/test/unit/styleMock.js',
    '^@/(.*)$': '<rootDir>/src/$1'
  },
  testURL: 'http://localhost/', // 避免 SecurityError: localStorage is not available for opaque origins 错误
  transform: {
    '\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$': '<rootDir>/test/unit/assets.transform.js',
    '^.+\\.js$': '<rootDir>/node_modules/babel-jest',
    '.*\\.(vue)$': '<rootDir>/node_modules/vue-jest',
    '\\.(css|styl|less|sass|scss)$': '<rootDir>/test/unit/styleMock.js'
  },
  testPathIgnorePatterns: ['<rootDir>/test/e2e'],
  snapshotSerializers: ['<rootDir>/node_modules/jest-serializer-vue'],
  setupFiles: ['<rootDir>/test/unit/setup'],
  // mapCoverage: true, // 移除,不再支援
  // 开启默认格式的测试覆盖率报告
  collectCoverage: true,
  coverageDirectory: '<rootDir>/test/unit/coverage',
  // 定义需要收集测试覆盖率信息的文件
  collectCoverageFrom: [
    'src/**/*.{js,vue}',
    '!src/main.js',
    '!src/router/index.js',
    '!**/node_modules/**',
    '!**/node_modules/**',
    '!src/App.vue'
  ],
  coverageReporters: [
    'lcov', // 会生成lcov测试结果以及HTML格式的漂亮的测试覆盖率报告
    'text' // 会在命令行界面输出简单的测试报告
  ]
}
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
38
39
40
可能遇见的问题:

TEST.png 解法:
Jest 中 testURL 的默认值是 about:blank,在 jsdom 环境下运行会报错,设置了 testURL 为一个有效的 URL 后能够避免这个问题,如:http://localhost。

简单测试范例

对照问题

  1. 被测试的对象是什么: + 运算符
  2. 要测试该对象的什么功能: 2 + 2 = 4
  3. 实际得到的结果:result
  4. 期望的结果: expectedResult
describe('Addition', () => {
    test('knows that 2 and 2 make 4', () => {
        expect(2 + 2).toBe(4);
    });
});
1
2
3
4
5

测试脚本里面应该包括一个或多个 describe 块,每个 describe 块应该包括一个或多个 test 块。
describe 块称为"测试套件"(test suite),表示一组相关的测试,给测试用例分组,方便管理。它是一个函数,第一个参数是测试套件的名称("加法函数的测试"),第二个参数是一个实际执行的函数。

describe('test testObject', () => {
    beforeAll(() => {
        // 预处理操作
    })
    test('is foo', () => {
        expect(testObject.foo).toBeTruthy()
    })
    test('is not bar', () => {
        expect(testObject.bar).toBeFalsy()
    })
    afterAll(() => {
        // 后处理操作
    })
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14

对测试文件中所有的用例进行统一的预处理,可以使用 beforeAll() 函数;而如果想在每个用例开始前进行都预处理,则可使用 beforeEach() 函数。至于后处理,也有对应的 afterAll() 和 afterEach() 函数。
详细断言文档
Jest

开始使用

TEST.png Jest 会自动找到项目中所有使用 .spec.js 或 .test.js 文件命名的测试文件并执行,
测试文件时遵循的命名规范:测试文件的文件名 = 被测试模块名 + .test.js。

在 unit/specs/functions.test.js 文件中创建测试用例
import functions from '../src/functions';
test('sum(2 + 2) 等于 4', () => {
    expect(functions.sum(2, 2)).toBe(4);
})
1
2
3
4
在 src/components/functions.js 中创建被测试的模块
export default {
    sum(a, b) {
        return a + b;
    }
}
1
2
3
4
5
运行 npm run test, Jest 会在 shell 中打印出以下消息
PASS test/functions.test.js
√ sum(2 + 2) 等于 4 (7ms)
 
Test Suites: 1 passed, 1 total
Tests: 1 passed, 1 total
Snapshots: 0 total
Time: 4.8s
1
2
3
4
5
6
7
常用的几种匹配器

.not 测试结果不等于某个值的情况

//functions.test.js
import functions from '../src/functions'
test('sum(2, 2) 不等于 5', () => {
    expect(functions.sum(2, 2)).not.toBe(5);
})
1
2
3
4
5

.toEqual() 会递归的检查对象所有属性和属性值是否相等

// functions.js
export default {
    getAuthor() {
        return {
            name: 'LITANGHUI',
            age: 24,
        }
    }
}
1
2
3
4
5
6
7
8
9
// functions.test.js
import functions from '../src/functions';
test('getAuthor()返回的对象深度相等', () => {
    expect(functions.getAuthor()).toEqual(functions.getAuthor());
})
test('getAuthor()返回的对象内存地址不同', () => {
    expect(functions.getAuthor()).not.toBe(functions.getAuthor());
})
1
2
3
4
5
6
7
8

..toThrow

// functions.test.js
import functions from '../src/functions';
test('getIntArray(3.3)应该抛出错误', () => {
    // 必须使用一个函数将将被测试的函数做一个包装 getIntArrayWrapFn(),否则会因为函数抛出导致该断言失败。
    function getIntArrayWrapFn() {
        functions.getIntArray(3.3);
    }
    expect(getIntArrayWrapFn).toThrow('"getIntArray"只接受整数类型的参数');
})
1
2
3
4
5
6
7
8
9

.toMatch 传入一个正则表达式,它允许我们用来进行字符串类型的正则匹配。

// functions.test.js
import functions from '../src/functions';
test('getAuthor().name应该包含"li"这个姓氏', () => {
    expect(functions.getAuthor().name).toMatch(/li/i);
})
1
2
3
4
5

真假值

  • toBeNull:只匹配null
  • toBeUndefined:只匹配undefined
  • toBeDefined:与toBeUndefined相反
  • toBeTruthy:匹配任何if语句为真
  • toBeFalsy:匹配任何if语句为假

数字

  • toBe:=
  • toEqual:=
  • toBeGreaterThan:>
  • toBeGreaterThanOrEqual:≥
  • toBeLessThan:<
  • toBeLessThanOrEqual:≤

字符串 -传入一个正则表达式,它允许我们用来进行字符串类型的正则匹配。

test('this is a joe in the sentence?', () => {
    expect('this is joe').toMatch(/joe/)
});
1
2
3

数组
.toContain- 数组中是否包含匹配子项

test('the Array contains the item?', () => {
    const arr = ['a', 'n', 'cc'];
    expect(arr).toContain('cc')
});
1
2
3
4

常用总整理

  • toBe 使用 Object.is 判断是否严格相等。
  • toEqual 递归检查对象或数组的每个字段。
  • toBeNull 只匹配 null。
  • toBeUndefined 只匹配 undefined。
  • toBeDefined 只匹配非 undefined。
  • toBeTruthy 只匹配真。
  • toBeFalsy 只匹配假。
  • toBeGreaterThan 实际值大于期望。
  • toBeGreaterThanOrEqual 实际值大于或等于期望值
  • toBeLessThan 实际值小于期望值。
  • toBeLessThanOrEqual 实际值小于或等于期望值。
  • toBeCloseTo 比较浮点数的值,避免误差。
  • toMatch 正则匹配。
  • toContain 判断数组中是否包含指定项。
  • toHaveProperty(keyPath, value) 判断对象中是否包含指定属性。
  • toThrow 判断是否抛出指定的异常。
  • toBeInstanceOf 判断对象是否是某个类的实例,底层使用 instanceof。

测试覆盖率

直接在命令中添加 --coverage 参数,或者在 package.json 文件进行更详细的配置。在项目下生产一个 coverage 目录,内附一个测试覆盖率的报告。
运行 jest --coverage 可看到生产的报告里展示了代码的覆盖率和未测试的行数

Mock

Mock 函数提供的以下三种特性,jest.fn()、jest.spyOn()、jest.mock(),在我们写测试代码时十分有用:

  • 捕获函数调用情况
  • 设置函数返回值
  • 改变函数的内部实现 jest.fn()
    不定义函数内部的实现,jest.fn() 会返回 undefined 作为返回值。
test('测试 jest.fn() 调用', () => {
    let mockFn = jest.fn();
    let result = mockFn(1, 2, 3);
 
    // 断言 mockFn 的执行后返回 undefined
    expect(result).toBeUndefined();
    // 断言 mockFn 被调用
    expect(mockFn).toBeCalled();
    // 断言 mockFn 被调用了一次
    expect(mockFn).toBeCalledTimes(1);
    // 断言 mockFn 传入的参数为1, 2, 3
    expect(mockFn).toHaveBeenCalledWith(1, 2, 3);
})
1
2
3
4
5
6
7
8
9
10
11
12
13
function forEach(items, callback) {
    for (let index = 0; index < items.length; index++) {
    callback(items[index]);
    }
}
 
const mockCallback = jest.fn();
forEach([0, 1], mockCallback);
 
// 判断是否被执行两次
expect(mockCallback.mock.calls.length).toBe(2);
 
// 判断函数被首次执行的第一个形参为 0
expect(mockCallback.mock.calls[0][0]).toBe(0);
 
// 判断函数第二次被执行的第一个形参为 1
expect(mockCallback.mock.calls[1][0]).toBe(1);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

jest.fn() 定义函数

test('测试 jest.fn() 返回固定值', () => {
    let mockFn = jest.fn().mockReturnValue('default');
    // 断言 mockFn 执行后返回值为 default
    expect(mockFn()).toBe('default');
})
 
test('测试 jest.fn() 内部实现', () => {
    let mockFn = jest.fn((num1, num2) => {
        return num1 * num2;
    })
    // 断言 mockFn 执行后返回 100
    expect(mockFn(10, 10)).toBe(100);
})
 
test('测试 jest.fn() 返回 Promise', async () => {
    let mockFn = jest.fn().mockResolvedValue('default');
    let result = await mockFn();
    // 断言 mockFn 通过 await 关键字执行后返回值为default
    expect(result).toBe('default');
    // 断言 mockFn 调用后返回的是 Promise 对象
    expect(Object.prototype.toString.call(mockFn())).toBe("[object Promise]");
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

实际应用

// fetch.js
 
import axios from 'axios';
export default {
    async fetchPostsList(callback) {
        return axios.get('https://jsonplaceholder.typicode.com/posts').then(res => {
            return callback(res.data);
        })
    }
}
 
// xxx.test.js
import fetch from '../src/fetch.js'
     
test('fetchPostsList 中的回调函数应该能够被调用', async () => {
    expect.assertions(1);
    let mockFn = jest.fn();
    await fetch.fetchPostsList(mockFn);
 
    // 断言 mockFn 被调用
    expect(mockFn).toBeCalled();
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

jest.mock()
events.js 引用同级下的 fetch.js

// fetch.js
 
import axios from 'axios';
 
export default {
    async fetchPostsList(callback) {
        return axios.get('https://jsonplaceholder.typicode.com/posts').then(res => {
            return callback(res.data);
        })
    }
}
1
2
3
4
5
6
7
8
9
10
11

// events.js
 
import fetch from './fetch';
 
export default {
    async getPostList() {
        return fetch.fetchPostsList(data => {
        console.log('fetchPostsList be called!');
        // do something
        });
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13

// functions.test.js
 
import events from '../src/events';
import fetch from '../src/fetch';
 
jest.mock('../src/fetch.js');
 
test('mock 整个 fetch.js 模块', async () => {
    expect.assertions(2);
    await events.getPostList();
    expect(fetch.fetchPostsList).toHaveBeenCalled();
    expect(fetch.fetchPostsList).toHaveBeenCalledTimes(1);
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14

调用了expect.assertions(2),它能确保在异步的测试用例中,有一个断言会在回调函数中被执行。
jest.spyOn()

// functions.test.js
 
import events from '../src/events';
import fetch from '../src/fetch';
 
test('使用jest.spyOn()监控fetch.fetchPostsList被正常调用', async() => {
    expect.assertions(2);
    const spyFn = jest.spyOn(fetch, 'fetchPostsList');
    await events.getPostList();
    expect(spyFn).toHaveBeenCalled();
    expect(spyFn).toHaveBeenCalledTimes(1);
})
1
2
3
4
5
6
7
8
9
10
11
12

通过 jest.spyOn(),fetchPostsList 被正常的执行了(可印出 fetchPostsList 的 console)。

jest.fn() 常被用来进行某些有回调函数的测试;jest.mock() 可以 mock 整个模块中的方法,当某个模块已经被单元测试 100% 覆盖时,使用 jest.mock() 去 mock 该模块,节约测试时间和测试的冗余度是十分必要;当需要测试某些必须被完整执行的方法时,常常需要使用 jest.spyOn()

BDD 测试

大家可能会想,有了单元测试,为何还需要 BDD 测试呢?BDD 主要是用来作验收测试,目的是缩短业务人员和开发之间的距离,让开发更高效。自动化也是高效的一个重要环节。

说明 BDD(Behavior Driven Development,行为驱动开发)前,我们先提端到端测试,简称 E2E(End-To-End test)测试,端到端测试将尽可能从使用者的视角,对真实系统的访问行为进行模拟。而 BDD 便是涵盖 E2E 所进行开发思维。

BDD(Behavior Driven Development 行为驱动开发)

  • 思路:与利益相关者(简单说就是客户)的讨论,取得对预期的软件行为的认识,其重点在于沟通
  • 过程:
    1. 从业务的角度定义具体的,以及可衡量的目标
    2. 找到一种可以达到设定目标的、对业务最重要的那些功能的方法
    3. 然后像故事一样描述出一个个具体可执行的行为。其描述方法基于一些通用词汇,这些词汇具有准确无误的表达能力和一致的含义。例如,expect, should, assert
    4. 寻找合适语言及方法,对行为进行实现
    5. 测试人员检验产品运行结果是否符合预期行为。最大程度的交付出符合用户期望的产品,避免表达不一致带来的问题
先写出业务共识下的使用者用例:
Feature: A reader can share an article to social networks
  As a reader
  I want to share articles
  So that I can notify my friends about an article I liked
  Scenario: An article was opened
    Given I'm inside an article
    When I share the article
    Then the article should change to a "shared" state
 
 
特性:读者可以在社交平台上分享一篇文章
  作为一名读者
  我想要分享文章
  这样我的朋友就知道我喜欢哪篇文章
  场景:打开一篇文章的链接
    假设我正在浏览这篇文章
    当我分享这篇文章后
    该文章变成"分享" 状态
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
再完成测试程式码:
// features/stepdefinitions/like-article.steps.js
module.exports = function() {
    this.Given(/^I'm inside an article$/, function(callback) {
        // 功能测试代码
    })
    this.When(/^I share the article$/, function(callback) {
        // 功能测试代码
    })
    this.Then(/^the article should change to a "shared" state$/, function(callback) {    
        // 功能测试代码
    })
}
1
2
3
4
5
6
7
8
9
10
11
12

主要区别就是在于测试的描述上,BDD 使用一种更通俗易懂的文字来描述测试用例。可参考使用者故事
我们在单元测试里我们提及到了 BDD,我们知道 BDD 使用一种更通俗易懂的文字来描述测试用例。通过 BDD 能够很方便让项目成员和业务干系人非常顺畅的沟通需求,即使这些成员不懂的任何编程语言。

// 英文 (不可中英混用)
 
Feature: Transferring money between accounts
  In order to manage my money more efficiently
  As a bank client
  I want to transfer funds between my accounts whenever I need to
  Scenario: Transferring money to a savings account
    Given my Current account has a balance of 1000.00
    And my Savings account has a balance of 2000.00
    When I transfer 500.00 from my Current account to my Savings account
    Then I should have 500.00 in my Current account
    And I should have 2500.00 in my Savings account
  Scenario: Transferring with insufficient funds
    Given my Current account has a balance of 1000.00
    And my Savings account has a balance of 2000.00
    When I transfer 1500.00 from my Current account to my Savings account
    Then I should receive an "insufficient funds" error
    Then I should have 1000.00 in my Current account
    And I should have 2000.00 in my Savings account
// 中文 (不可中英混用)
# language: zh-CN
功能: 多个 github 仓库查看
作为一个 github 使用者
我想已帐号密码登入 github
查看已订阅的不同用户 github 仓库
 
  场景: 订阅多个 github 仓库
    假如 认证用户登录
    当 订阅不同用户下的 github 仓库:
      | owner        | project |
      | imtesteruser | abc     |
      | cuketest     | demos   |
    那么 订阅列表中应该包含这些项目
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

BDD 框架

BDD 框架对比: Cucumber.js vs Robot Framework vs Gauge.js TEST.png 选用 Cucumber 原因:

  • 使用自然语言,更易读
  • 支持表格参数
  • 支持多种格式的 Report:html、junit etc.
  • 支持 javascript 语言
  • 支持四种状态的测试步骤:Passed、Failed、Skipped、Pending
  • 支持使用变形器消除重复
// 中文 (不可中英混用)
 
# language: zh-CN
功能: 失败的登录
  场景大纲: 失败的登录
    假设 我在网站的首页
    当 输入用户名 <用户名>
    当 输入密码 <密码>
    当 提交登录信息
    那么 页面应该返回 "Error Page"
 
    例子:
    |用户名 |密码 |
    |'Jan1' |'password'|
    |'Jan2' |'password'|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const defineSupportCode = require('cucumber').defineSupportCode
const expect = require('chai').expect
defineSupportCode(function({Given, When, Then}) {
    Given('我在网站的首页', function() {
        return this.driver.get('http://0.0.0.0:7272/');
    });
    When('输入用户名 {string}', function (text) {
        return this.driver.findElement(By.id('username_field')).sendKeys(text)
    });
    When('输入密码 {string}', function (text) {
        return this.driver.findElement(By.id('password_field')).sendKeys(text)
    });
    When('提交登录信息', function () {
        return this.driver.findElement(By.id('login_button')).click()
    });
    Then('页面应该返回 {string}', function (string) {
        this.driver.getTitle().then(function(title) {
        expect(title).to.equal(string);
        });
    });
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

Robot Framework

DSL Code Examples
*** Settings ***
Documentation     登录测试 2
...
Suite Setup       打开浏览器到登录页1
Suite Teardown    Close Browser
Test Setup        转到登录页
Test Template     使用错误的失败凭据应该登录失败
Resource          resource.robot

*** Test Cases ***               USER NAME        PASSWORD
无效的用户名                      invalid          ${VALID PASSWORD}
无效的密码                        ${VALID USER}    invalid
无效的用户名和密码                 invalid          whatever

*** Keywords ***
使用错误的失败凭据应该登录失败
    [Arguments]    ${username}    ${password}
    输入用户名    ${username}
    输入密码    ${password}
    提交登录信息
    登录应该不成功

登录应该不成功
    Location Should Be    ${ERROR URL}
    Title Should Be    Error Page
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
Step Code Examples
打开浏览器到登录页
    Open Browser    ${LOGIN URL}    ${BROWSER}
    Maximize Browser Window
    Set Selenium Speed    ${DELAY}
    Login Page Should Be Open

Login Page Should Be Open
    Title Should Be    Login Page

转到登录页
    Go To    ${LOGIN URL}
    Login Page Should Be Open

输入用户名
    [Arguments]    ${username}
    Input Text    username_field    ${username}

输入密码
    [Arguments]    ${password}
    Input Text    password_field    ${password}

提交登录信息
    Click Button    login_button

应该跳转到欢迎页
    Location Should Be    ${WELCOME URL}
    Title Should Be        Welcome Page
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

可执行的规格说明书这一理念 Cucumber 把需求规格说明书、自动化测试和在线文档合并成了一种格式 Gherkin
行为驱动开发(BDD)是一系列软件工程实践,它的设计目的是帮助团队更快地开发及交付更有价值的、质量更高的软件。从需求分析到生产环境中的代码开发,通过可执行规范与自动化测试支撑着整个流程。
BDD 实践者将具体的实例表述为可执行的场景,它使用一种半结构化的格式“given……when……then”,这种格式对于干系人与团队成员来说都很容易阅读。(但后来不再用此方法)
故事映射(Story Mapping)
DD、ATDD、实例化需求(Specification by Example)?
清晰的验收场景能够帮助我们理解要创建什么、不要创建什么,并且理解为什么这样选择。

BDD report

BDD 测试,BDD 的报表产出,格式为 JSON + XML ,为让更好阅读,藉由插件产出较好阅读的 HTML 格式。

✓ Step1 安装 report 插件 cucumber-html-reporter

TEST.png

✓ Step2 在根目录下建立 cucumble-html-reports.js,内容下
const reporter = require('cucumber-html-reporter');
var options = {
	theme: 'bootstrap',
	jsonFile: 'test/e2e/reports/cucumber.json',
	output: 'test/e2e/reports/cucumber_report.html',
	reportSuiteAsScenarios: true,
	launchReport: true,
};
reporter.generate(options);
1
2
3
4
5
6
7
8
9
✓ Step3 修改 package.json
"scripts": {
	"e2e": "node test/e2e/runner.js && node ./cucumble-html-		reports.js",
},
1
2
3
✓ Step4 产出报表
npm run e2e
1

Nightwatch

Vue-Cli 里为我们准备了一个当下很流行的 E2E 测试框架 - Nightwatch,使用 Selenium WebDriver API 以将 Web 应用测试自动化

Vue-Cli 下的 Nightwatch

TEST.png ▵ Nightwatch 具体的工作流程

vue-lic 下 Nightwatch 的文档结构
└── test
    └── e2e
        ├── custom-assertions // 自定义断言
        │   └── elementCount.js
        ├── page-objects // 页面对象文件夹
        ├── reports // 输出报表文件夹
        ├── screenshots // 自动截屏
        ├── nightwatch.conf.js // nightwatch 运行配置
        ├── runner.js // 运行器
        └── specs // 测试文件
            └── test.spec.js
1
2
3
4
5
6
7
8
9
10
11
Nightwatch 配置
require('babel-register')
const config = require('../../config')
 
// http://nightwatchjs.org/gettingstarted#settings-file
module.exports = {
  // == 基本配置 == //
  src_folders: ['test/e2e/specs'],  // BDD 測試文件位置
  output_folder: 'test/e2e/reports',  // report 放置位置
  custom_assertions_path: ['test/e2e/custom-assertions'],  // 自制断言位置
 
  // == Selenium 配置 ==  //
  selenium: {  // selenium 服务器配置
    start_process: true,  // 配置是否自动管理 selenium 进程
    server_path: require('selenium-server').path,  // 运行文件目录
    host: '127.0.0.1',
    port: 4444,
    cli_args: {
      'webdriver.chrome.driver': require('chromedriver').path
    }
  },
   
  // == 测试环境配置 == //   
  test_settings: {  // 测试配置
    default: {
      selenium_port: 4444,
      selenium_host: 'localhost',
      silent: true,
      globals: {
        devServerURL: 'http://localhost:' + (process.env.PORT || config.dev.port)
      }
    },
 
    chrome: {
      desiredCapabilities: {
        browserName: 'chrome',
        javascriptEnabled: true,
        acceptSslCerts: true
      }
    },
 
    firefox: {
      desiredCapabilities: {
        browserName: 'firefox',
        javascriptEnabled: true,
        acceptSslCerts: true
      }
    }
  }
}
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
38
39
40
41
42
43
44
45
46
47
48
49

以上配置分三大类

  • 基本配置;
  • Selenium 配置;
  • 测试环境配置。

TEST.png Nightwatch 配置参数说明 TEST.png Selenium 配置参数说明 TEST.png 测试环境 配置参数说明

加入 Cucumber 框架

Nightwatch 具有很好的扩展性与兼容性,能集成最正统的 BDD 测试框架 Cucumber ,为让 nightwatch 可以配合 cucumber 使用,这里用上 nightwatch-cucumber 插件,(现已不再维护)。

安装插件
npm i nightwatch-cucumber cucumber geckodriver cucumber-pretty nightwatch-helpers -D
or
yarn add nightwatch-cucumber cucumber geckodriver cucumber-pretty nightwatch-helpers --dev
1
2
3
nightwatch.conf.js 配置
require('babel-register')
require('nightwatch-cucumber')({ // cucumber 参数
    cucumberArgs: [
        '--require',
        'test/e2e/features/step_definitions',
        '--format',
        'node_modules/cucumber-pretty',
        '--format',
        'json:test/e2e/reports/cucumber.json',
        'test/e2e/features'
    ]
})
const seleniumServer = require('selenium-server')
const chromedriver = require('chromedriver') // chrome
const geckodriver = require('geckodriver') // firefox
const config = require('../../config') // http://nightwatchjs.org/gettingstarted#settings-file
module.exports = {
    src_folders: ['test/e2e/features/step_definitions'],
    output_folder: 'test/e2e/reports',
    // 加入 尤大的扩展 API
    custom_commands_path: ['./node_modules/nightwatch-helpers/commands'],
    custom_assertions_path: ['./node_modules/nightwatch-helpers/assertions'],
    selenium: {
        start_process: true,
        server_path: seleniumServer.path,
        host: '127.0.0.1',
        port: 4444,
        cli_args: {
            'webdriver.chrome.driver': chromedriver.path
        }
    },
    test_settings: {
        default: {
            selenium_port: 4444,
            selenium_host: 'localhost',
            silent: true,
            globals: {
                devServerURL: 'http://localhost:' + (process.env.PORT || config.dev.port)
            },
            desiredCapabilities: {
                browserName: 'chrome',
                javascriptEnabled: true,
                acceptSslCerts: true,
                chromeOptions: {
                    args: ['incognito', 'headless', 'no-sandbox', 'disable-gpu']
                }
            },
            selenium: {
                cli_args: {
                    'webdriver.chrome.driver': chromedriver.path
                }
            }
        },
        chrome: {
            desiredCapabilities: {
                browserName: 'chrome',
                javascriptEnabled: true,
                acceptSslCerts: true
            },
            selenium: {
                cli_args: {
                    'webdriver.chrome.driver': chromedriver.path
                }  
            }
        },
        firefox: {
            desiredCapabilities: {
                browserName: 'firefox',
                javascriptEnabled: true,
                acceptSslCerts: true
            },
            selenium: {
            cli_args: {
                'webdriver.gecko.driver': geckodriver.path
                }
            }
        }
    }
}
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
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
建立 feature 架构
├── features // cucumber
│ ├── step_definitions // cucumber
│ └── support // cucumber
1
2
3

覆盖率

单元测试覆盖率(code coverage) TEST.png

  • 语句覆盖率(statement coverage):是否每个语句都执行了
  • 分支覆盖率(branch coverage):是否每个 if 代码块都执行了
  • 函数覆盖率(function coverage):是否每个函数都调用了
  • 行覆盖率(line coverage):是否每一行都执行了
Sonarqube report

对代码库中的代码进行分析之前,我们需要搭建持续集成工具(如:Jenkins),并在工具中集成 SonarQube Scanners,根据持续集成工具设置的条件会自动触发拉取和 Build 代码,然后经过 SonarQube Scanners 分析并将分析报告发送到 SonarQube Server,SonarQube Server 对分析报告进行处理并保存到 SonarQube Database,同时可将分析报告发送给相关负责人进行 Review ,最终我们可以通过 UI 界面进行查看分析结果,开发人员对有问题的代码再次进行优化,如此循环。

✓ Step1 在根目录建立 sonar-project.properties
sonar.projectKey=javascript-prj
sonar.projectName=JavaScript Demo Project
sonar.projectVersion=1.0
sonar.sources=src
sonar.sourceEncoding=UTF-8
sonar.host.url=http://10.211.62.227:9001
# sonar.login=admin
# sonar.password=admin
sonar.language=js
# sonar.modules=javascript-module
#sonar.modules=java-module,javascript-module,html-module,css-module#设定unit test的code coverage的报告的路径
#sonar.ts.coverage.lcovReportPath=<lcov.info文件的路径>
#JS的code coverage report的属性
sonar.javascript.lcov.reportPaths=./test/unit/coverage/lcov.info
1
2
3
4
5
6
7
8
9
10
11
12
13
14

如此在提交文件时,即会跑单元测试覆盖率审核与产出报表。

Last Updated: 2019-8-14 6:29:41 PM