保持设置代码在特定的测试范围内

在可能的情况下,资源和依赖关系的设置应该尽可能地与具体的测试用例密切相关。例如,给定一个设置函数:

// mustLoadDataSet loads a data set for the tests.
//
// This example is very simple and easy to read. Often realistic setup is more
// complex, error-prone, and potentially slow.
func mustLoadDataset(t *testing.T) []byte {
    t.Helper()
    data, err := os.ReadFile("path/to/your/project/testdata/dataset")

    if err != nil {
        t.Fatalf("could not load dataset: %v", err)
    }
    return data
}

在需要的测试函数中明确调用mustLoadDataset

// Good:
func TestParseData(t *testing.T) {
    data := mustLoadDataset(t)
    parsed, err := ParseData(data)
    if err != nil {
        t.Fatalf("unexpected error parsing data: %v", err)
    }
    want := &DataTable{ /* ... */ }
    if got := parsed; !cmp.Equal(got, want) {
        t.Errorf("ParseData(data) = %v, want %v", got, want)
    }
}

func TestListContents(t *testing.T) {
    data := mustLoadDataset(t)
    contents, err := ListContents(data)
    if err != nil {
        t.Fatalf("unexpected error listing contents: %v", err)
    }
    want := []string{ /* ... */ }
    if got := contents; !cmp.Equal(got, want) {
        t.Errorf("ListContents(data) = %v, want %v", got, want)
    }
}

func TestRegression682831(t *testing.T) {
    if got, want := guessOS("zpc79.example.com"), "grhat"; got != want {
        t.Errorf(`guessOS("zpc79.example.com") = %q, want %q`, got, want)
    }
}

测试函数TestRegression682831不使用数据集,因此不调用mustLoadDataset,这可能会很慢且容易失败:

// Bad:
var dataset []byte

func TestParseData(t *testing.T) {
    // As documented above without calling mustLoadDataset directly.
}

func TestListContents(t *testing.T) {
    // As documented above without calling mustLoadDataset directly.
}

func TestRegression682831(t *testing.T) {
    if got, want := guessOS("zpc79.example.com"), "grhat"; got != want {
        t.Errorf(`guessOS("zpc79.example.com") = %q, want %q`, got, want)
    }
}

func init() {
    dataset = mustLoadDataset()
}

用户希望在与其他函数隔离的情况下运行一个函数,不应受到这些因素的影响:

# No reason for this to perform the expensive initialization.
$ go test -run TestRegression682831

何时使用自定义的 TestMain 入口点 #

如果包中的所有测试都需要共同设置,并且设置需要拆解,你可以使用自定义测试主入口。如果测试用例所需的资源的设置特别昂贵,而且成本应该被摊销,就会发生这种情况。通常情况下,你在这一点上已经从测试套件中提取了任何无关的测试。它通常只用于功能测试

使用自定义的 TestMain 不应该是你的首选,因为要正确使用它,必须要有足够的谨慎。首先考虑摊销普通测试设置部分的解决方案或普通的测试辅助函数是否足以满足你的需求。

// Good:
var db *sql.DB

func TestInsert(t *testing.T) { /* omitted */ }

func TestSelect(t *testing.T) { /* omitted */ }

func TestUpdate(t *testing.T) { /* omitted */ }

func TestDelete(t *testing.T) { /* omitted */ }

// runMain sets up the test dependencies and eventually executes the tests.
// It is defined as a separate function to enable the setup stages to clearly
// defer their teardown steps.
func runMain(ctx context.Context, m *testing.M) (code int, err error) {
    ctx, cancel := context.WithCancel(ctx)
    defer cancel()

    d, err := setupDatabase(ctx)
    if err != nil {
        return 0, err
    }
    defer d.Close() // Expressly clean up database.
    db = d          // db is defined as a package-level variable.

    // m.Run() executes the regular, user-defined test functions.
    // Any defer statements that have been made will be run after m.Run()
    // completes.
    return m.Run(), nil
}

func TestMain(m *testing.M) {
    code, err := runMain(context.Background(), m)
    if err != nil {
        // Failure messages should be written to STDERR, which log.Fatal uses.
        log.Fatal(err)
    }
    // NOTE: defer statements do not run past here due to os.Exit
    //       terminating the process.
    os.Exit(code)
}

理想情况下,一个测试用例在自身的调用和其他测试用例之间是密封的。

至少要确保单个测试用例重置他们所修改的任何全局状态,如果他们已经这样做了(例如,如果测试是与外部数据库一起工作)。

摊销共同测试设置 #

如果普通设置中存在以下情况,使用 sync.Once 可能是合适的,尽管不是必须的。

  • 它很昂贵。
  • 它只适用于某些测试。
  • 它不需要拆解。
// Good:
var dataset struct {
    once sync.Once
    data []byte
    err  error
}

func mustLoadDataset(t *testing.T) []byte {
    t.Helper()
    dataset.once.Do(func() {
        data, err := os.ReadFile("path/to/your/project/testdata/dataset")
        // dataset is defined as a package-level variable.
        dataset.data = data
        dataset.err = err
    })
    if err := dataset.err; err != nil {
        t.Fatalf("could not load dataset: %v", err)
    }
    return dataset.data
}

mustLoadDataset 被用于多个测试函数时,其成本被摊销:

// Good:
func TestParseData(t *testing.T) {
    data := mustLoadDataset(t)

    // As documented above.
}

func TestListContents(t *testing.T) {
    data := mustLoadDataset(t)

    // As documented above.
}

func TestRegression682831(t *testing.T) {
    if got, want := guessOS("zpc79.example.com"), "grhat"; got != want {
        t.Errorf(`guessOS("zpc79.example.com") = %q, want %q`, got, want)
    }
}

普通拆解之所以棘手,是因为没有统一的地方来注册清理线程。如果设置函数(本例中为loadDataset)依赖于上下文,sync.Once可能会有问题。这是因为对设置函数的两次调用中的第二次需要等待第一次调用完成后再返回。这段等待时间不容易做到尊重上下文的取消。