What is useful for t.Cleanup?

15.1k Views Asked by At

Questions

I would like to know the use case of t.Cleanup introduced in Go1.14. What is the convenience of t.Cleanup as compared to using defer?

https://golang.org/pkg/testing/#T.Cleanup.

  • Sample

For example, let's say we create a temporary directory, and when we test it, we want to delete the temporary directory we created.

t.Cleanup can be used to write a test as follows, but it also works as defer os.RemoveAll(tempDir).

package mypkg

import (
    "io/ioutil"
    "os"
    "testing"
)

func TestDirwalk(t *testing.T) {
    tempDir, err := ioutil.TempDir(".", "temp")
    if err != nil {
        t.Errorf("create tempDir: %v", err)
    }
    t.Cleanup(func() { os.RemoveAll(tempDir) })

    // something...
}
3

There are 3 best solutions below

2
On BEST ANSWER

Cleanup functions are also called if your test panics, so in your case both would work.

The advantage of using T.Cleanup() becomes clear if your test calls other functions, passing testing.T along. Obviously using defer in those functions would be executed before those functions return, but if you register cleanup functions using T.Cleanup(), then they will be called only at the end of your test.

Think of T.Cleanup() as an "improved" and extended version of defer. It also documents that the passed functions are for cleanup purposes.

1
On

Besides what others have pointed out, t.Cleanup() is also useful when dealing with parallel subtests, where the cleanup should only run after all subtests have completed. Consider

func TestSomething(t *testing.T){
   setup()
   defer cleanup()
   t.Run("parallel subtest 1", func(t *testing.T){
      t.Parallel()
      (...)
   })
   t.Run("parallel subtest 2", func(t *testing.T){
      t.Parallel()
      (...)
   })
}

which doesn't work because the test function will return while the the subtests are still running, causing the resources required by the subtests to get wiped out by defer cleanup().

Before t.Cleanup() a way to solve this was to wrap the subtests on another test

func TestSomething(t *testing.T){
   setup()
   defer cleanup()
   t.Run("parallel tests", func(t *testing.T){
      t.Run("subtest 1", func(t *testing.T){
         t.Parallel()
         (...)
      })
      t.Run("subtest 2", func(t *testing.T){
         t.Parallel()
         (...)
      })
   })
}

which looks ok, but with t.Cleanup() it gets way better

func TestSomething(t *testing.T){
   setup()
   t.Cleanup(cleanup)
   t.Run("parallel subtest 1", func(t *testing.T){
      t.Parallel()
      (...)
   })
   t.Run("parallel subtest 2", func(t *testing.T){
      t.Parallel()
      (...)
   })
}
3
On

t.Cleanup is useful for cleaning up resources allocated by a helper function when the test does not care about the resource itself.

Example

Consider testing a service layer. The service uses a *sql.DB but does not create it itself.

package testutils

import (
  "testing"

  "my/db"
  "my/domain"
)

func NewTestSubject(t *testing.T) *domain.Service {
  t.Helper()  
  sqldb := newDatabase(t)
  s, _ := domain.NewService(sqldb)
  return s
}

func newDatabase(t *testing.T) *sql.DB {
  t.Helper()
  d, _ := db.Create()
  t.Cleanup(func() {
    d.Close()
  })
}

Without t.Cleanup newTestSubject would have to return (*domain.Service, *sql.DB), leaking details about domain.Service's construction.