Go 1.14 中 Cleanup 方法简介
原文:What's New In Go 1.14: Test Cleanup
单元测试通常遵循某些步骤。首先,建立单元测试的依赖关系;接下来运行测试的逻辑;然后,比较测试结果是否达到我们的期望;最后,清除测试时的依赖关系,为避免影响其他单元测试要将测试环境还原。在Go1.14中,testing 包现在有了 testing.(*T).Cleanup 方法,其目的是更加容易地创建和清除测试的依赖关系。
一般的测试
通常,应用会有某些 类似于存储库 的结构,用作对数据库的访问。测试这些结构可能有点挑战性,因为测试时会更改数据库的数据状态。通常,测试会有个函数实例化该结构对象:
1. func NewTestTaskStore(t *testing.T) *pg.TaskStore {
2. store := &pg.TaskStore{
3. Config: pg.Config{
4. Host: os.Getenv("PG_HOST"),
5. Port: os.Getenv("PG_PORT"),
6. Username: "postgres",
7. Password: "postgres",
8. DBName: "task_test",
9. TLS: false,
10. },
11. }
12.
13. err = store.Open()
14. if err != nil {
15. t.Fatal("error opening task store: err:", err)
16. }
17.
18. return store
19. }
这为我们提供了一个支持Postgres存储的新商店实例,该实例负责在任务跟踪程序中存储不同的任务。现在我们可以生成此存储的实例,并为其编写一个测试:
1. func Test_TaskStore_Count(t *testing.T) {
2. store := NewTestTaskStore(t)
3.
4. ctx := context.Background()
5. _, err := store.Create(ctx, tasks.Task{
6. Name: "Do Something",
7. })
8. if err != nil {
9. t.Fatal("error creating task: err:", err)
10. }
11.
12. tasks, err := store.All(ctx)
13. if err != nil {
14. t.Fatal("error fetching all tasks: err:", err)
15. }
16.
17. exp := 1
18. got := len(tasks)
19.
20. if exp != got {
21. t.Error("unexpected task count returned: got:", got, "exp:", exp)
22. }
23. }
该测试的目的是好的——确保在创建一个任务后仅返回一个任务。当运行该测试后它通过了:
$ export PG_HOST=127.0.0.1
$ export PG_PORT=5432
$ go test -count 1 -v ./...
? github.com/timraymond/cleanuptest [no test files]
=== RUN Test_TaskStore_LoadStore
--- PASS: Test_TaskStore_LoadStore (0.01s)
=== RUN Test_TaskStore_Count
--- PASS: Test_TaskStore_Count (0.01s)
PASS
ok github.com/timraymond/cleanuptest/pg 0.035s
因为测试框架将缓存测试通过并假定测试会继续通过,所以必须在这些测试中添加 -count 1 绕过测试缓存。当再次允许测试时,测试失败了:
$ go test -count 1 -v ./...
? github.com/timraymond/cleanuptest [no test files]
=== RUN Test_TaskStore_LoadStore
--- PASS: Test_TaskStore_LoadStore (0.01s)
=== RUN Test_TaskStore_Count
Test_TaskStore_Count: pg_test.go:79: unexpected task count returned: got: 2 exp: 1
--- FAIL: Test_TaskStore_Count (0.01s)
FAIL
FAIL github.com/timraymond/cleanuptest/pg 0.029s
FAIL
使用 defer 清除依赖
测试不会自动清除环境依赖,因此现有状态会使以后的测试结果无效。最简单的修复方法是在测试完后使用defer函数清除状态。由于每个使用 TaskStore 的测试都必须这样做,因此从实例化 TaskStore 的函数中返回一个清理函数是有意义的:
1. func NewTestTaskStore(t *testing.T) (*pg.TaskStore, func()) {
2. store := &pg.TaskStore{
3. Config: pg.Config{
4. Host: os.Getenv("PG_HOST"),
5. Port: os.Getenv("PG_PORT"),
6. Username: "postgres",
7. Password: "postgres",
8. DBName: "task_test",
9. TLS: false,
10. },
11. }
12.
13. err := store.Open()
14. if err != nil {
15. t.Fatal("error opening task store: err:", err)
16. }
17.
18. return store, func() {
19. if err := store.Reset(); err != nil {
20. t.Error("unable to truncate tasks: err:", err)
21. }
22. }
23. }
在第18-21行,返回一个调用 * pg.TaskStore 的 Reset 方法的闭包,该闭包从作为第一个参数返回的中调用。在测试中,我们必须确保在defer中调用该闭包:
1. func Test_TaskStore_Count(t *testing.T) {
2. store, cleanup := NewTestTaskStore(t)
3. defer cleanup()
4.
5. ctx := context.Background()
6. _, err := store.Create(ctx, tasks.Task{
7. Name: "Do Something",
8. })
9. if err != nil {
10. t.Fatal("error creating task: err:", err)
11. }
12.
13. tasks, err := store.All(ctx)
14. if err != nil {
15. t.Fatal("error fetching all tasks: err:", err)
16. }
17.
18. exp := 1
19. got := len(tasks)
20.
21. if exp != got {
22. t.Error("unexpected task count returned: got:", got, "exp:", exp)
23. }
24. }
现在测试正常了,如果需要更多的defer调用,代码就会越来越臃肿。如何保证每一个都会执行到?如果某一个defer执行时painc了怎么办?这些额外的工作分散了对测试的专注。此外,如果测试必须要考虑这些动态部分,测试会越来越困难。如果想更容易点测试,则需要编写更多的代码。
使用 Cleanup
Go1.14引入了 testing.(* T).Cleanup 方法,可以注册对测试者透明运行的清理函数。现在用 Cleanup 重构工厂函数:
1. func NewTestTaskStore(t *testing.T) *pg.TaskStore {
2. store := &pg.TaskStore{
3. Config: pg.Config{
4. Host: os.Getenv("PG_HOST"),
5. Port: os.Getenv("PG_PORT"),
6. Username: "postgres",
7. Password: "postgres",
8. DBName: "task_test",
9. TLS: false,
10. },
11. }
12.
13. err = store.Open()
14. if err != nil {
15. t.Fatal("error opening task store: err:", err)
16. }
17.
18. t.Cleanup(func() {
19. if err := store.Reset(); err != nil {
20. t.Error("error resetting:", err)
21. }
22. })
23.
24. return store
25. }
NewTestTaskStore 函数仍然需要 *testing.T 参数,如果不能连接 Postgres 测试会失败。在18-22行,调用 Cleanup 方法,并使用包含store的Reset方法的func作为参数。不像 defer 那样,func 会在每个测试的最后去执行。集成到测试函数:
1. func Test_TaskStore_Count(t *testing.T) {
2. store := NewTestTaskStore(t)
3.
4. ctx := context.Background()
5. _, err := store.Create(ctx, cleanuptest.Task{
6. Name: "Do Something",
7. })
8. if err != nil {
9. t.Fatal("error creating task: err:", err)
10. }
11.
12. tasks, err := store.All(ctx)
13. if err != nil {
14. t.Fatal("error fetching all tasks: err:", err)
15. }
16.
17. exp := 1
18. got := len(tasks)
19.
20. if exp != got {
21. t.Error("unexpected task count returned: got:", got, "exp:", exp)
22. }
23. }
在第2行,只接收了从NewTestTaskStore 返回的 *pg.TaskStore。很好地封装了构建*pg.TaskStore的函数只处理清除依赖和错误处理,因此可以仅专注于测试的东西。
关于t.Parallel
使用 testing.(*T).Parallel() 方法能让测试,子测试在单独的 Goroutines 中执行。仅需要在测试中调用 Parallel() 就能和其他调用 Parallel()的测试一起安全地运行。修改之前的测试开启多个一样的子测试:
1. func Test_TaskStore_Count(t *testing.T) {
2. ctx := context.Background()
3. for i := 0; i < 10; i++ {
4. t.Run(fmt.Sprintf("%d", i), func(t *testing.T) {
5. t.Parallel()
6. store := NewTestTaskStore(t)
7. _, err := store.Create(ctx, cleanuptest.Task{
8. Name: "Do Something",
9. })
10. if err != nil {
11. t.Fatal("error creating task: err:", err)
12. }
13.
14. tasks, err := store.All(ctx)
15. if err != nil {
16. t.Fatal("error fetching all tasks: err:", err)
17. }
18.
19. exp := 1
20. got := len(tasks)
21.
22. if exp != got {
23. t.Error("unexpected task count returned: got:", got, "exp:", exp)
24. }
25. })
26. }
27. }
使用 t.Run() 方法在 for 循环中开启10个子测试。因为都调用了 t.Parallel(),所有的子测试可以并发运行。把创建store 也放到子测试中,因为 store 中的 t 实际上是子测试的 *testing.T。再添加些log验证清除函数是否执行。运行go test 看下结果:
=== CONT Test_TaskStore_Count/3
=== CONT Test_TaskStore_Count/8
=== CONT Test_TaskStore_Count/9
=== CONT Test_TaskStore_Count/2
=== CONT Test_TaskStore_Count/4
=== CONT Test_TaskStore_Count/1
Test_TaskStore_Count/3: pg_test.go:77: unexpected task count returned: got: 3 exp: 1
Test_TaskStore_Count/3: pg_test.go:31: cleanup!
Test_TaskStore_Count/5: pg_test.go:77: unexpected task count returned: got: 4 exp: 1
Test_TaskStore_Count/5: pg_test.go:31: cleanup!
Test_TaskStore_Count/9: pg_test.go:77: unexpected task count returned: got: 4 exp: 1
Test_TaskStore_Count/9: pg_test.go:31: cleanup!
Test_TaskStore_Count/2: pg_test.go:77: unexpected task count returned: got: 4 exp: 1
Test_TaskStore_Count/2: pg_test.go:31: cleanup!
=== CONT Test_TaskStore_Count/7
=== CONT Test_TaskStore_Count/6
Test_TaskStore_Count/8: pg_test.go:77: unexpected task count returned: got: 0 exp: 1
Test_TaskStore_Count/8: pg_test.go:31: cleanup!
像预期的那样,清除函数在子测试结束时执行了,这是因为使用了子测试的 *testing.T。然而,测试仍然失败了,因为一个子测试结果仍然对其他的子测试可见,这是因为没有使用事务。
然而在并行子测试中 t.Cleanup() 是有用的,在本例中最好使用。在测试中结合使用 Cleanup 函数和事务,可能会有更多成功。
总结
t.Cleanup 的“神奇”行为对于我们在Go中的惯用法似乎太机智了。但我也不希望在生产代码中使用这种机制。测试和生产代码在很多方面不同,因此放宽一些条件以更容易编写测试代码和更容易阅读测试内容。就像 t.Fatal 和 t.Error 使处理测试中的错误变得微不足道一样,t.Cleanup 有望使保留清理逻辑变得更加容易,而不会像 defer 那样使测试混乱。
Go 1.14 中 Cleanup 方法简介的更多相关文章
- simplify the life ECMAScript 5(ES5)中bind方法简介
一直以来对和this有关的东西模糊不清,譬如call.apply等等.这次看到一个和bind有关的笔试题,故记此文以备忘. bind和call以及apply一样,都是可以改变上下文的this指向的.不 ...
- ECMAScript 5(ES5)中bind方法简介备忘
一直以来对和this有关的东西模糊不清,譬如call.apply等等.这次看到一个和bind有关的笔试题,故记此文以备忘. bind和call以及apply一样,都是可以改变上下文的this指向的.不 ...
- jq中 load()方法 简介
load()方法会在元素的onload事件中绑定一个处理函数.如果处理函数绑定给window对象,则会在所有内容(包括窗口,框架,对象和图像等)加载完毕后触发,如果处理函数绑定在元素上,则会在元素的内 ...
- C++中 _itoa_s方法简介
_itoa_s 函数原型如下: _itoa_s ( int value, char *buffer, size_t sizeInCharacters, //存放结果的字符数组长度 int radix ...
- Mapper类/Reducer类中的setup方法和cleanup方法以及run方法的介绍
在hadoop的源码中,基类Mapper类和Reducer类中都是只包含四个方法:setup方法,cleanup方法,run方法,map方法.如下所示: 其方法的调用方式是在run方法中,如下所示: ...
- iOS中常用的四种数据持久化方法简介
iOS中常用的四种数据持久化方法简介 iOS中的数据持久化方式,基本上有以下四种:属性列表.对象归档.SQLite3和Core Data 1.属性列表涉及到的主要类:NSUserDefaults,一般 ...
- Python Python-MySQLdb中的DictCursor使用方法简介
Python-MySQLdb中的DictCursor使用方法简介 by:授客 QQ:1033553122 DictCursor的这个功能是继承于CursorDictRowsMixIn,这个Mi ...
- zz神经网络模型量化方法简介
神经网络模型量化方法简介 https://chenrudan.github.io/blog/2018/10/02/networkquantization.html 2018-10-02 本文主要梳理了 ...
- 数学相关函数在PHP中的应用简介
对于数学计算来说,最常见的其实还是我们使用各种操作符的操作,比如说 +加.-减 之类的.当然,PHP 中也为我们提供了一些可以方便地进行其他数学运算的操作函数.这些函数都属于 Math 扩展.这个扩展 ...
随机推荐
- spring boot 整合 swagger2
swagger2为了更好的管理api文档接口 swagger构建的api文档如下,清晰,避免了手写api诸多痛点 一,添加依赖 <!--swagger2的官方依赖--> <depen ...
- javaweb-codereview 学习记录-3
Class类加载流程 实际上就是ClassLoader将会调用loadclass来尝试加载类,首先将会在jvm中尝试加载我们想要加载的类,如果jvm中没有的话,将调用自身的findclass,此时要是 ...
- 18年第一弹射 和网络有关; 艾曲塞嗯诶系列篇 two
35: 华为AR G3系列路由器可以通过FTP和TFTP更新系统文件,AR G3系列路由器可以作为FTP Client , FTP Server ,TFTP Client 36: 两台路由器间通过串口 ...
- oracle 11g数据库服务器安装
系统:windows7旗舰版 64位.oracle数据库服务器版本:oracle11g. 一.下载 1.登录oracle账户: 首先打开谷歌浏览器,输入网址[英文版网址:https://www.o ...
- JSON Web Token 是什么?
免费获得官方JWT手册并深入学习JWT吧! 简介 JSON Web Token(缩写JWT),是一套开放的标准(RFC 7519),它定义了一种紧凑且自URL安全的方式,以JSON对象的方式在各方之间 ...
- learn more ,study less(三):超越整体性学习
高效率的学生 成为一名高效率学生或是自学者需 要掌握减少花在书本上时间的艺术,我上学时,除了全日制的上课学习,业余时间经营一家 企业,每周写大约 7000 字,健身以及主持一家演讲俱乐部,尽管如此,我 ...
- JQuery--50个必备的实用jQuery代码段.
原文出处:http://my.oschina.net/chengjiansunboy/blog/55496?p=2#comments 1. 如何修改jQuery默认编码(例如默认UTF-8改成改GB2 ...
- 面试总结 | Linux后台开发不得不看的知识点(给进军bat的你!)
目录 一 自我介绍 二 面试情况 三 相关知识点汇总 1 c/c++相关 2 计算机网络 3 数据结构相关 4 数据库相关 5 操作系统 6 Linux基础知识及应用编程(后台必备!) 7 大数问题 ...
- Spring Boot入门简介-Maven配置
一.简介 -- 简化Spring应用开发的一个框架: -- 整个Spring技术栈的一个大整合: -- J2EE开发的一站式解决方案. 二.背景: ① J2EE笨重的开发.繁多的配置.低下的开发效率. ...
- 使用 FRP 让部门同事都能直接远程桌面办公( 适用于 TEAM 和向日葵卡顿的用户)
背景说明 这两天由于疫情的原因,很多公司都得在家远程上班,然后像我们这类小公司有没有 VPN 这些东西.传统的远程回公司只能依靠 Teamviewer 或者向日葵等工具.但是由于最近用户量很多,可能会 ...