测试基本认知
为什么需要测试
测试保证了软件的质量和可靠性,确保软件是按照预期的功能进行的。
发现和修复
通过测试,可以提前发现一些功能不完整、性能低下、有安全漏洞的地方,从而进行一个修复操作。
如果一个产品没有经过测试就直接进行交付,那么前面的这些缺陷最终就会在用户使用的时候暴露出来,最终导致用户的流失。
需求和标准
验证软件是否符合需求和标准。在软件开发中,需求和标准的定义往往是通过需求文档、设计文档以及用户操作流程等方式来确定,那么根据这些文档以及操作流程来编写测试用例,最终测试用例通过,可以间接的说明我们的软件是符合需求和标准。
降低维护成本
软件的维护成本是一个非常重要的考虑因素,如果软件存在缺陷和问题,那么软件上线后这些问题才被发现并去修复,整个维护成本会大大增加。
一般来讲,越到后期,修复一个Bug所付出的成本越大,因此通过测试能够提前帮助开发团队在软件上线之前就发现潜在的问题并进行修复,从而降低了软件的维护成本。
增强开发者信心
在软件开发领域,也存在Bug破窗效应,指的是软件中如果存在一个Bug,那么这个Bug可能会引起一个连锁反应,从未导致一堆的Bug。
一旦软件中出现Bug破窗,就会极大的影响团队的士气,并且整个团队需要为修复这些Bug付出极大的时间和精力。
遵循最佳实践
无论是哪一种开发模式(瀑布模式、敏捷开发模式、DevOps),测试都是非常重要的一个环节,可以算是一种软件开发的最佳实践,是整个软件开发中不可缺少的一环,进行测试实际上就是在遵循软件开发的最佳实践。
测试的种类
整个测试从下往上可以分为4类:
- 静态测试
- 单元测试
- 集成测试
- E2E测试

静态测试
静态测试不会涉及到具体的代码的运行,它是在编写代码期间对代码进行一个检查和分析,捕获写代码时可能出现的语法错误或错别字。
对于前端开发人员来讲,静态测试更倾向于使用TypeScript或者ESlint之类的静态检查工具,在编写代码时候就能够提示错误
// ESLint for-direction 规则能让你更早的发现问题
for (var i = 0; i < 10; i--) {
console.log(i)
}
const two = "10";
// 使用 TypeScript 的静态检查功能也是能够提前发现问题
const result = add(1, two);
单元测试
单元测试往往是验证某一个单独的部分是否能够正常工作,它是软件测试中的最小测试单位,通常是一个函数或者一个方法。
单元测试往往是由开发人员来编写一个一个的测试用例,通过一些自动化的工具来进行测试。
function calculateSum(a, b) {
return a + b;
}
describe("calculateSum", function() {
it("should add two numbers correctly", function() {
// 期望传入 1,2 的时候得到的值为 3
expect(calculateSum(1, 2)).toEqual(3);
// 期望传入 3,4 的时候得到的值为 7
expect(calculateSum(3, 4)).toEqual(7);
});
});
单元测试由于是对一个函数或者方法进行测试,是独立的一个单元,因此在进行单元测试的时候,往往会屏蔽发送请求,连接数据库等功能,这些功能一般都通过Mock(模拟)的形式来实现。
集成测试
集成测试就是将多个单元组装起来一起进行测试,主要是看这些单元在一起的时候是否能够正常工作,也就是说,集成测试的目的是确保整个系统中各个部分连接起来是能够正常工作的。
到了集成测试这个阶段,就会连接真实的数据库,发送真实的网络请求,确保它们在协作的时候能够正常的工作。
const request = require("supertest");
const app = require("./app");
describe("User API", function() {
let userId;
it("should add a new user", function(done) {
request(app)
.post("/users")
.send({ name: "Alice", email: "alice@example.com" })
.expect(200)
.end(function(err, res) {
if (err) return done(err);
userId = res.body.id;
done();
});
});
});
E2E测试
End To End,翻译成中文就是端到端的测试。这种测试就会测试整个软件系统的功能以及完整性。
这种测试会去模拟用户的行为和软件进行一个交互,相比集成测试,E2E测试会测试更加完整的功能,更像是一个真实的用户在和软件进行交互。
import {generate} from 'todo-test-utils'
describe('todo app', () => {
it('should work for a typical user', () => {
const user = generate.user()
const todo = generate.todo()
cy.visitApp()
cy.findByText(/register/i).click()
cy.findByLabelText(/username/i).type(user.username)
cy.findByLabelText(/password/i).type(user.password)
cy.findByText(/login/i).click()
cy.findByLabelText(/add todo/i)
.type(todo.description)
.type('{enter}')
cy.findByTestId('todo-0').should('have.value', todo.description)
cy.findByLabelText('complete').click()
cy.findByTestId('todo-0').should('have.class', 'complete')
})
})
上面的代码描述了一个完整的流程,从打开应用程序到注册用户、创建待办事项、完成待办事项、直到最终验证应用程序的状态,这就是一个典型的 E2E 测试,它会验证整个应用程序的功能和用户体验。
项目驱动模式
TDD模式
Test-Driven Development,即测试驱动开发。TDD模式是一种以测试为中心的开发方法,强调在编写代码之前先编写测试用例,然后再运行测试用例,如果测试用例失败了,就说明代码有问题,那么就需要修改代码直到所有的测试用例都通过,然后再去编写实际的代码。
- 编写测试用例。
const sum = require('../sum');
// 测试用例
test('adds 1 + 2 to equal 3', () => {
// 期望传递 1,2 的时候得到 3
expect(sum(1, 2)).toBe(3);
});
// 测试用例
test('adds 0 + 0 to equal 0', () => {
// 期望传递 0,0 的时候得到 0
expect(sum(0, 0)).toBe(0);
});
// 测试用例
test('adds -1 + 1 to equal 0', () => {
// 期望传递 -1, 1 的时候得到 0
expect(sum(-1, 1)).toBe(0);
});
- 运行测试用例,如果测试用例跑不通,说明代码设计有问题,注意这个时候实际代码是还没有书写的,因此这个时候往往会通过一些Mock(模拟) 手段去模拟函数或者模块的实现。
- 编写实际的代码,通过编写实际代码来替换上一步中所用到的Mock的部分。
BDD模式
Behavior-Driven Development,即行为驱动开发。BDD是通过行为来驱动软件的开发。这里的行为实际上指的是用户的行为,也就是说BDD的模式关注焦点并非在技术实现的细节上面,而是在用户行为和业务上面,更加注重协作和沟通。
BDD的测试用例一般会采用自然语言来进行编写,以便与业务人员和QA都能读得懂该测试用例。
BDD的测试用例一般会采用Given-When-Then的模式来描述测试场景。
例如下面是一个基于此模式的测试用例,假设我们要测试一个登陆页面,这个登陆页面里面包含用户名和密码框以及登陆按钮,测试用例如下:
- Given:用户已经打开登录页面,并且没有输入任何内容。
- When:用户输入错误的用户名或密码,然后点击登录按钮。
- Then:页面上会显示错误提示信息"用户名或密码错误"。
这是一种非常常见的模式,很多中小型企业,在没有使用自动化的测试框架的背景下,往往就是通过这种方式来对软件进行测试。