Go 1.14 中 Cleanup 要领简介
.net 微服务实践
单元测试平常遵照某些步骤。起首,竖立单元测试的依靠关联;接下来运转测试的逻辑;然后,比较测试效果是不是到达我们的希冀;末了,消灭测试时的依靠关联,为防止影响其他单元测试要将测试环境复原。在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
那样使测试杂沓。
Hibernate入门之命名策略(naming strategy)详解