单元测试(unit test)除用来测试逻辑算法是否符合预期外,还承担着监控代码质量的责任。任何时候都可用简单的命令来验证全部功能,找出未完成任务(验收)和任何因修改而造成的错误。它与性能测试、代码覆盖率等一起保障了代码总是在可控范围内,这远比形式化的人工检查要有用得多。
单元测试并非要取代人工代码审查(code review),实际上它也无法切入到代码实现层面。但可通过测试结果为审查提供筛选依据,避免因烦琐导致代码审查沦为形式主义。单元测试可自动化进行,能持之以恒。但测试毕竟只是手段,而非目的,所以如何合理安排测试就需要开发人员因地制宜。
可将测试、版本管理工具,以及自动发布(nightly build)整合。编写脚本将测试失败结果与代码提交日志相匹配,最终生成报告发往指定邮箱。
很多人认为单元测试代码不好写,不知道怎么测试。如果非技术原因,那么需要考虑结构设计是否合理,因为可测试性也是代码质量的一个体现。
在我看来,写单元测试本身就是对即将要实现的算法做复核预演。因为无论什么算法都需要输入条件,返回预期结果。这些,加上平时写在main里面的临时代码,本就是一个完整的单元测试用例,无非换个地方存放而已。
testing
工具链和标准库自带单元测试框架,这让测试工作变得相对容易。
- 测试代码须放在当前包以“_test.go”结尾的文件中。
- 测试函数以Test为名称前缀。
- 测试命令(go test)忽略以“_”或“.”开头的测试文件。
- 正常编译操作(go build/install)会忽略测试文件。
main_test.go
package main
import(
"testing"
)
func add(x,y int)int{
return x+y
}
func TestAdd(t*testing.T) {
if add(1,2) !=3{
t.FailNow()
}
}
输出:
$go test-v # 要测试当前包及所有子包,可用go test./...
===RUN TestAdd
---PASS:TestAdd(0.00s)
PASS
ok test 0.006s
标准库testing提供了专用类型T来控制测试结果和行为。
方法 说明 相关
------------------+-----------------------------+-----------------
Fail 失败:继续执行当前测试函数
FailNow 失败:立即终止执行当前测试函数 Failed
SkipNow 跳过:停止执行当前测试函数 Skip,Skipf,Skipped
Log 输出错误信息。仅失败或 -v时输出 Logf
Parallel 与有同样设置的测试函数并行执行
Error Fail+Log Errorf
Fatal FailNow+Log Fatalf
使用Parallel可有效利用多核并行优势,缩短测试时间。
func TestA(t*testing.T) {
t.Parallel()
time.Sleep(time.Second*2)
}
func TestB(t*testing.T) {
if os.Args[len(os.Args)-1] == "b" {
t.Parallel()
}
time.Sleep(time.Second*2)
}
输出:
$go test-v
---PASS:TestB(2.00s)
---PASS:TestA(2.00s)
PASS
ok test 4.014s
$go test-v-args"b"
---PASS:TestA(2.00s)
---PASS:TestB(2.00s)
PASS
ok test 2.009s
从测试总耗时可以看到并行执行的结果只有2秒。
只有一个测试函数调用Parallel方法并没有效果,且go test执行参数parallel必须大于1。
常用测试参数:
参数 说明 示例
------------+-----------------------------------+------------------------
-args 命令行参数
-v 输出详细信息
-parallel 并发执行,默认值为GOMAXPROCS -parallel 2
-run 指定测试函数,正则表达式 -run"Add"
-timeout 全部测试累计时间超时将引发panic,默认值为10ms -timeout 1m30s
-count 重复测试次数,默认值为1
对于测试是否应该和目标放在同一目录,一直有不同看法。某些人认为应该另建一个专门的包用来存放单元测试,且只测试目标公开接口。好处是,当目标内部发生变化时,无须同步维护测试代码。每个人对于测试都有不同理解,就像覆盖率是否要做到90%以上,也是见仁见智。