在实际研发与测试工作中,单元测试是代码走向高质量的必经之路,也是效能优化实践的重要一环。单元是应用的最小可测试部件。在过程化编程中,一个单元就是单个程序、函数、过程等;对于面向对象编程,最小单元就是方法,包括基类、超类、抽象类等中的方法。单元测试就是软件开发中对最小单位进行正确性检验的测试工作。
不同地方对单元测试有的定义可能会有所不同,但有一些基本共识:
从软件/项目工程上面来说:
软件工程的3个基本要素就是过程,方法,工具, 而质量作为软件工程的根基,是必须保证的。
单元测试可以检查软件工程中3要素的质量,是软件工程必须要经历的一个环节。
随着业务复杂度的上升,软件工程至少经历了3次变革,从早期的瀑布模型到后来发展的V模型再到原型实现模型。
不管软件开发过程如何变化,测试始终是一个非常重要的阶段。可以很直接地说没有做过测试的软件开发,就是假开发, 也没有质量一说。
从工程师自己来说:
分析需求,明确测试重点和难点
a. 优先考虑对核心逻辑代码进行测试
b.优先针对于容易出错、没有信心的部分代码做测试
C. 优先考虑对存在状态变化的代码进行测试
d. 优先保证产品周期,非致命问题可以右移
使用monkey等mock/stub工具
使用覆盖率,变异测试, bug率等指标。
遇到需求变动,先改测试用例,再改逻辑。
先重构代码,在考虑如何写测试。
编程时, 应该保证代码的可测性, 需要遵循至少以下3点:
可以按阶段来推进这一部分工作并且借助高层的力量。我们可以分如下2个阶段:
好的单元测试需要遵循测试的FIRST原则
除了FIRST中提到这几点,一个好的单元测试还应该具备以下能力:
在实际编写代码过程中,不同的团队会有不同团队的风格,只要团队内部保持有一定的规约即可,比如:
规范可以自定义, 也可以参考《golang测试用例规范》
单元测试是要写额外的代码的,这对开发同学的也是一个不小的工作负担,在一些项目中,我们合理的评估单元测试的编写,我认为我们不能走极端,当然理论上来说全写肯定时好的,但是从成本,效率上来说我们必须做出权衡,衡量原则如下:
规范(规格)导出法将需求”翻译“成测试用例。
例如,一个函数的设计需求如下:
函数:一个计算平方根的函数
输入: 实数
输出: 实数
要求: 当输入一个0或者比0大的实数时,返回其正的平方根;当输入一个小于0的实数时,显示错误信息“平方根非法—输入之小于0”,并返回0;库函数 printf()
可以用来输出错误信息。 |
在这个规范中有3个陈述,可以用两个测试用例来对应:
等价类划分法假定某一特定的等价类中的所有值对于测试目的来说是等价的,所以在每个等价类中找一个之作为测试用例。
有效等价类 | 无效等价类 |
---|---|
6~18个字符(1) | 少于6个字符(2) 多余18个字符(3) 空(4) |
包含字母、数字、下划线(5) | 除字母、数字、下划线的特殊字符(6) 非打印字符(7) 中文字符 (8) |
以字母开头(9) | 以数字或下划线开头(10) |
测试用例:
编号 | 输入数据 | 覆盖等价类 | 预期结果 |
---|---|---|---|
1 | test_111 | (1)、(5)、(9) | 合法输入 |
2 | t_11 | (2)、(5)、(9) | 非法输入 |
3 | testtesttest_12345678 | (3)、(5)、(9) | 非法输入 |
4 | NULL | (4) | 非法输入 |
5 | test!@1111 | (1)、(6)、(9) | 非法输入 |
6 | test 1111 | (1)、(7)、(9) | 非法输入 |
7 | test测试1111 | (1)、(8)、(9) | 非法输入 |
8 | _test111 | (1)、(5)、(10) | 非法输入 |
边界值分析法使用与等价类测试方法相同的等价类划分,只是边界值分析假定
错误更多地存在于两个划分的边界上。
边界值测试在软件变得复杂的时候也会变得不实用。边界值测试对于非向量类型的值(如枚举类型的值)也没有意义。
例如,和4.1相同的需求:
划分(ii)的边界为0和最大正实数;划分(i)的边界为最小负实数和0。由此得到以下测试用例:
基本路径测试法是在程序控制流图的基础上,通过分析控制构造的环路复杂性,导出基本可执行路径集合,从而设计测试用例的方法。设计出的测试用例要保证在测试中程序的每个可执行语句至少执行一次。
基本路径测试法的基本步骤:
虽然目前并没有直接的指标去衡量单测的质量,但是我们可以通过一些间接手段保证单元测试的质量。
以下是一些常用的用来检查单元测试质量的的指标:
测试驱动开发是敏捷开发中的一项核心实践和技术,也是一种设计方法论,TDD首先考虑使用需求(对象、功能、过程、接口等)。主要是编写测试用例框架对功能的过程和接口进行设计,而测试框架可以持续进行验证。大行其道的一些模式对TDD的支持都非常不错,比如MVC和MVP等。
BDD也就是行为驱动开发。这里的B并非指的是Business,实际上BDD可以看作是对TDD的一种补充,让开发、测试、BA以及客户都能在这个基础上达成一致,JBehave之类的BDD框架。
通过单元测试用例来驱动功能代码的实现,团队需要定义出期望的质量标准和验收细则,以明确而且达成共识的验收测试计划(包含一系列测试场景)来驱动开发人员的TDD实践和测试人员的测试脚本开发。面向开发人员,强调如何实现系统以及如何检验。
测试用例(Test Case)是指对一项特定的软件产品进行测试任务的描述,体现测试方案、方法、技术和策略。其内容包括测试目标、测试环境、输入数据、测试步骤、预期结果、测试脚本等,最终形成文档。简单地认为,测试用例是为某个特殊目标而编制的一组测试输入、执行条件以及预期结果,用于核实是否满足某个特定软件需求
测试报告是指把测试的过程和结果写成文档,对发现的问题和缺陷进行分析,为纠正软件的存在的质量问题提供依据,同时为软件验收和交付打下基础。测试报告的内容可以总结为以下目录:
黑盒测试 (Black Box Testin)又叫数据驱动测试,本质上就是功能测试。把测试对象当做一个黑盒子,测试时,对程序内部的逻辑结构和内部特性,完全不需要考虑。根据需求说明书,测试程序的功能,是否符合它的说明。
白盒测试 (white-box testing)又称为结构测试或逻辑驱动测试。本质上就是通过代码检查的方式进行测试.把测试对象看做一个打开的盒子,测试人员用程序内部的逻辑结构、有关信息,设计或选择测试用例,对程序所有逻辑路径展开测试。在不同的点检查程序状态,确定实际状态,是否与预期的状态一致。
灰盒测试(Grey Box Testing)是介于白盒测试与黑盒测试之间。可以这样理解,灰盒测试关注输出对于输入的正确性,同时也关注内部表现,但这种关注不象白盒那样详细、完整,只是通过一些表征性的现象、事件、标志来判断内部的运行状态,有时候输出是正确的,但内部其实已经错误了。这种情况非常多,如果每次都通过白盒测试来操作,效率会很低,因此需要采取这样的一种灰盒的方法。
更多测试分类请参考https://www.cnblogs.com/findyou/p/6480411.html
在选择 Stub/Mock框架前简单说一下这2个词的意思。如果被测程序、系统或对象,我们称之为A,那么Stub和Mock指的并不是A,而是测A的过程中,A需要与之交互的程序、系统或对象B。为了测试A而又不会影响B,我们通常需要一个B的“替身”。
Stub,也即“桩”,很早就有这个说法了,也有人说“打桩”,主要出现在集成测试的过程中,从上往下的集成时,作为下方程序的替代。作用如其名,就是在需要时,能够发现它存在,即可。就好像点名,“到”即可。
Mock,主要是指某个程序的傀儡,也即一个虚假的程序,可以按照测试者的意愿做出响应,返回被测对象需要得到的信息。也即是要风得风、要雨得雨、要返回什么值就返回什么值。
编写代码时,我们总是会做出一些假设,断言就是用于在代码中捕捉这些假设。程序员相信在程序中的某个特定点该表达式值为真,可以在任何时候启用和禁用断言验证,因此可以在测试时启用断言而在部署时禁用断言。同样,程序投入运行后,最终用户在遇到问题时可以重新启用断言。
使用断言可以创建更稳定、品质更好且 不易于出错的代码。当需要在一个值为FALSE时中断当前操作的话,可以使用断言。单元测试必须使用断言(Junit/JunitX)。
测试左移可以降低成本提高效率,预防bug比修复bug的成本要低得多。
测试右移可以降低试错成本,提升问题拦截率,降低影响面。
测试左移和右移,构成一个完整的研发和运营质量闭环,前后贯穿整个质量体系,一起构建质量墙。
这里的执行者,并不限定只能是测试人员。包括产品、开发、测试、运维、运营等等,全员都可以承担里面的任务。
质量是没有边界的,项目是大家共同的。
由于Golang原生没有提供断言,所以我们需要考虑引入Golang的一些断言组件,下面是几种比较常见的测试框架
测试框架 | 推荐指数 |
---|---|
Go自带的testing包 | ★★☆☆☆ |
GoConvey | ★★★★★ |
Testify | ★★★★☆ |
github地址:https://github.com/smartystreets/goconvey
特性:
github地址:https://github.com/stretchr/testify
特性:
Golang有以下几种Stub/Mock框架:
测试框架 | 推荐指数 |
---|---|
GoStub | ★★☆☆☆ |
GoMock | ★★☆☆☆ |
GoMonkey | ★★★★★ |
考虑到gomonkey的功能比较齐全,对代码侵入小,故选择 GoMonkey框架做“替身”,下面简单列一下这几个框架的介绍。
github地址:https://github.com/prashantv/gostub
详见:《golang测试框架gostub的使用》
特性:
缺陷:
github地址:https://github.com/golang/mock
详见:《go测试框架gomock的使用》
特性:
缺陷:
github地址:https://github.com/bouk/monkey
详见:《go测试框架gomonkey的使用》
特性:
缺陷:
github地址: https://github.com/DATA-DOG/go-sqlmock
特性:
缺陷:
sqlmock只适合用在简单的场景, 业务实际使用的时候更多还是建议在docker里起一个mysql, 然后把测试数据载入db里面并做成自动化流水线, 这种方式会比sqlmock高效很多, 不过这需要完善的基础设施和运维经验.
github地址: https://github.com/alicebob/miniredis
假如程序里用到 Redis,要伪造一个 Redis Client 用之前的办法也是可以的。miniredis
是在 Golang 程序中运行的 Redis Server,它实现了大部分原装 Redis 的功能,测试的时候miniredis.Run()
然后将 Redis Client 连向 miniredis 就可以了。
import (
...
"github.com/alicebob/miniredis/v2"
...
)
func TestSomething(t *testing.T) {
s := miniredis.RunT(t)
// Optionally set some keys your code expects:
s.Set("foo", "bar")
s.HSet("some", "other", "key")
// Run your code and see if it behaves.
// An example using the redigo library from "github.com/gomodule/redigo/redis":
c, err := redis.Dial("tcp", s.Addr())
_, err = c.Do("SET", "foo", "bar")
// Optionally check values in redis...
if got, err := s.Get("foo"); err != nil || got != "bar" {
t.Error("'foo' has the wrong value")
}
// ... or use a helper for that:
s.CheckGet(t, "foo", "bar")
// TTL and expiration:
s.Set("foo", "bar")
s.SetTTL("foo", 10*time.Second)
s.FastForward(11 * time.Second)
if s.Exists("foo") {
t.Fatal("'foo' should not have existed anymore")
}
}
详情参见:《Go测试框架-Mock http请求》
单元测试中还有个难题是如何伪造 HTTP 请求的结果。如果像上面那样封装一下,可能会漏掉一些极端情况的测试,比如连接网络出错,失败的状态码。Golang 有个 httptest 库,可以在 test 时创建一个 server,让 client 连上 server。这样做会有点绕,事实上 Golang 的 http.Client 有个 Transport 成员,输入输出都通过它,通过篡改 Transport 就可以返回我们需要的数据。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。