这是我参与「第五届青训营 」伴学笔记创作活动的第 4 天.
测试
在实际开发过程中,测试关系着系统的质量,质量决定了线上系统的稳定性,一旦出现bug就会造成事故。测试一般分为:
- 回归测试:通常QA同学手动通过终端回归一些固定的主流程场景进行测试。
- 集成测试:对系统功能维度做测试验证。
- 单元测试:测试开发阶段,开发者对单独的函数、模块做功能验证。 层级从上至下,测试成本减低,覆盖率提升,所以单元测试的覆盖率也决定了代码的质量。
总之,测试是避免事故发生的最后一道屏障,这章会分别介绍单元测试,Mock测试,以及基准测试。
单元测试
单元测试包括:输入、测试单元(包括接口,函数,模块等)、输出、以及校对(确保代码的功能与预期相符)。
- 单元测试可以保证质量:在整体的覆盖率足够的情况下,一定程度保证了新功能本身的正确性,也未破坏原有代码的正确性。
- 单元测试也可以提升效率:在代码有bug的情况下,编写单元测试可以在短周期内定位和修复问题。
单元测试-规则
单元测试的规范:
- 所有测试文件以_test.go结尾(publish_post_test.go)。
- 函数名以Test开头,且连接的第一个字母大写(
TestPublishPost(t *testing.T){})。 - 初始化逻辑放到TestMain中。
- 运行时
go test [flags][packages]或ide直接运行。
func TestMain(m *testing.m) {
//测试前:数据装载、配置初始化等前置工作
code := m.Run()
//测试后:释放资源等收尾工作
os.Exit(code)
}
代码示例:
单元测试-覆盖率
衡量代码覆盖率可以通过指定cover参数(go test example_test.go example.go --cover)。
假设代码一共三行,单元测试执行了两行,覆盖率就为66.66667%。
Tips:实际项目中,通常覆盖率要求在50%-60%之间,对于资金型服务,覆盖率要求达到80%以上。单元测试的分支相互独立,全面覆盖,所以要求函数体足够小,这样就能比较简单的提升覆盖率,也符合函数设计的单一职责。
单元测试-依赖
复杂项目中一般会依赖数据库、Cache(redis组件)、或者本地文件。单元测试的目标是使程序幂等和稳定。幂等是指每一次测试运行都能产生与之前一样的结果;稳定是指相互隔离,能在任何时间,任何环境,运行测试。需要实现这些目的,就要用到Mock机制。
举例:一个读取文件函数可以将文件的第一行字符串中的‘11’替换成‘00’,执行单元测试,测试通过,但是单元测试需要依赖本地的文件,如果本地的文件被修改或者删除,测试就会fail。为了保证测试case的稳定性,我们对读取文件函数进行Mock,来屏蔽对文件的依赖。
Mock测试
Monkey是一个开源的mock测试库,可以对method,或者实例的方法进行mock,反射,指针赋值。
Mock的使用案例(通过patch对ReadFirstLine进行打桩mock):通过patch对ReadFirstLine进行打桩mock,始终返回line110,再通过defer卸载mock,就摆脱了测试函数对本地文件的依赖。
基准测试
基准测试是指测试一段程序的运行性能和耗费CPU的程度,可以用于在实际开发中定位问题并做性能分析。使用方法类似于单元测试。
举例:假设有10个服务器,每次随机执行select函数随机选择一个执行。
基准测试以Benchmark开头,入参是testing.B,用b种的N值反复递增循环测试(对一个测试用例的默认测试时间是1秒,如果测试函数返回时还不到1秒,那么testing.B中的N值将按1、2、5、10、20、50.....递增,并以递增后的值重新进行用例函数测试)。Resttimer重置计时器,我们在reset之前做了init或其他的准备操作,这些操作不应该作为基准测试的范围;runparallel是多协程并发测试,通过执行2个基准测试,发现代码在并发情况下存在劣化,主要原因是rand为了保证全局的随机性和并发安全,持有了一把全局锁。
这一随机性能问题优化方式:通过高性能随机数方法fastrand(字节开源github.com/bytedance/gopkg),性能提升了百倍。主要思路:牺牲一定的数列一致性,在大多场景适用。
总结:
整个章节涉及三个部分:单元测试,Mock测试,和基准测试。当项目开发完成后,需要用单元测试来提升项目质量和可靠性;当有外部依赖时,可以通过mock来保证单元测试的稳定性;基准测试可以通过本地测试分析系统性能。