zoukankan      html  css  js  c++  java
  • 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 方法,并使用包含storeReset方法的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.Fatalt.Error 使处理测试中的错误变得微不足道一样,t.Cleanup 有望使保留清理逻辑变得更加容易,而不会像 defer 那样使测试混乱。

  • 相关阅读:
    20182316胡泊 实验5报告
    20182316胡泊 第6周学习总结
    20182316胡泊 第5周学习总结
    20182316胡泊 实验4报告
    20182316胡泊 实验3报告
    20182316胡泊 第4周学习总结
    20182316胡泊 第2,3周学习总结
    实验2报告 胡泊
    《数据结构与面向对象程序设计》第1周学习总结
    实验1报告
  • 原文地址:https://www.cnblogs.com/YYRise/p/12376689.html
Copyright © 2011-2022 走看看