ChatGPT解决这个技术问题 Extra ChatGPT

如何使用 Go 中的测试包进行测试设置

在使用 testing package 时,如何进行整体测试设置处理,从而为所有测试奠定基础?

作为 Nunit 中的示例,有一个 [SetUp] 属性。

[TestFixture]
public class SuccessTests
{
  [SetUp] public void Init()
  { /* Load test data */ }
}

0
030

从 Go 1.4 开始,您可以实现 setup/teardown(无需在每次测试之前/之后复制您的函数)。 Main 部分中here概述了该文档:

TestMain 在主 goroutine 中运行,并且可以围绕调用 m.Run 进行任何必要的设置和拆卸。然后它应该使用 m.Run 的结果调用 os.Exit

我花了一些时间才弄清楚这意味着如果一个测试包含一个函数 func TestMain(m *testing.M) 那么这个函数将被调用而不是运行测试。在这个函数中,我可以定义测试将如何运行。例如我可以实现全局设置和拆卸:

func TestMain(m *testing.M) { setup() code := m.Run() shutdown() os.Exit(code) }

其他几个示例 can be found here

在最新版本中添加到 Go 的测试框架中的 TestMain 功能是针对多个测试用例的简单解决方案。 TestMain 提供了一个全局钩子来执行设置和关闭,控制测试环境,在子进程中运行不同的代码,或者检查测试代码泄漏的资源。大多数软件包不需要 TestMain,但在需要它的时候它是一个受欢迎的补充。


TestMain 曾经在一个包中,所以它不是很有用。我发现 subtests 更适合更复杂的用途。
你应该如何在不使用全局变量的情况下将上下文从设置函数传递给测试?例如,如果 mySetupFunction() 创建了一个临时目录来执行测试(使用唯一的随机名称),测试如何知道目录的名称?必须有一个地方来设置这个上下文??
看来这是官方处理测试钩子前后的方法,官方文档见golang.org/pkg/testing/#hdr-Main
@InancGumus lstat $GOROOT/subtests: no such file or directory
请注意,'code := m.Run()' 是运行其他 TestFunctions 的那个!
m
miltonb

这可以通过在 myfile_test.go 文件中放置一个 init() 函数来实现。这将在 init() 函数之前运行。

// myfile_test.go
package main

func init() {
     /* load test data */
}

myfile_test.init() 将在包 init() 函数之前调用。


我知道您正在回答自己的问题,因此这可能满足您自己的用例,但这并不等同于您在问题中包含的 NUnit 示例。
很公平。您在此答案中显示的内容更接近于使用 NUnit 的 [TestFixtureSetUp] 属性。
它不包括拆卸部分
如果您的测试文件与 main 函数在同一个包中,这不是一个好的解决方案。
你犯错了吗?它说 init() 函数将在 init() 函数之前运行,我想这首先是不可能的,但我怀疑你的意思是在任何测试函数之前说?
K
Kare Nuorteva

给定一个简单的单元测试函数:

package math

func Sum(a, b int) int {
    return a + b
}

您可以使用返回拆卸功能的设置功能对其进行测试。在调用 setup() 之后,您可以延迟调用 teardown()。

package math

import "testing"

func setupTestCase(t *testing.T) func(t *testing.T) {
    t.Log("setup test case")
    return func(t *testing.T) {
        t.Log("teardown test case")
    }
}

func setupSubTest(t *testing.T) func(t *testing.T) {
    t.Log("setup sub test")
    return func(t *testing.T) {
        t.Log("teardown sub test")
    }
}

func TestAddition(t *testing.T) {
    cases := []struct {
        name     string
        a        int
        b        int
        expected int
    }{
        {"add", 2, 2, 4},
        {"minus", 0, -2, -2},
        {"zero", 0, 0, 0},
    }

    teardownTestCase := setupTestCase(t)
    defer teardownTestCase(t)

    for _, tc := range cases {
        t.Run(tc.name, func(t *testing.T) {
            teardownSubTest := setupSubTest(t)
            defer teardownSubTest(t)

            result := Sum(tc.a, tc.b)
            if result != tc.expected {
                t.Fatalf("expected sum %v, but got %v", tc.expected, result)
            }
        })
    }
}

Go 测试工具将在 shell 控制台中报告日志记录语句:

% go test -v
=== RUN   TestAddition
=== RUN   TestAddition/add
=== RUN   TestAddition/minus
=== RUN   TestAddition/zero
--- PASS: TestAddition (0.00s)
    math_test.go:6: setup test case
    --- PASS: TestAddition/add (0.00s)
        math_test.go:13: setup sub test
        math_test.go:15: teardown sub test
    --- PASS: TestAddition/minus (0.00s)
        math_test.go:13: setup sub test
        math_test.go:15: teardown sub test
    --- PASS: TestAddition/zero (0.00s)
        math_test.go:13: setup sub test
        math_test.go:15: teardown sub test
    math_test.go:8: teardown test case
PASS
ok      github.com/kare/go-unit-test-setup-teardown 0.010s
% 

您可以使用这种方法将一些额外的参数传递给 setup/teardown。


现在这是一个真正简单但有效的技巧。很好地使用 Go 语法。
是的,但它增加了嵌套性(javascript 中的一种厄运金字塔)。而且,测试不会像外部测试那样由套件自动运行。
另一个问题是tearDown func无法获取每个案例的数据,并根据测试结果进行清理
m
mdwhatcott

Go 测试框架没有任何与 NUnit 的 SetUp attribute 等效的东西(标记要在套件中的每个测试之前调用的函数)。不过有几个选项:

只需在需要的每个测试中调用您的 SetUp 函数。使用实现 xUnit 范式和概念的 Go 测试框架的扩展。想到三个强有力的选择:gocheck 作证 gunit

这些库中的每一个都鼓励您将测试组织到类似于其他 xUnit 框架的套件/夹具中,并将在每个 Test* 方法之前调用套件/夹具类型的设置方法。


b
baijum

通常,go 中的测试与其他语言的编写风格不同。通常,测试函数相对较少,但每个函数都包含一组表驱动的测试用例。请参阅this article written by one of the Go team.

对于表驱动测试,您只需将任何设置代码放在执行表中指定的各个测试用例的循环之前,然后再放置任何清理代码。

如果您仍然在测试函数之间共享设置代码,则可以将共享设置代码提取到一个函数中,如果重要的是它只执行一次很重要,则可以使用 sync.Once(或者按照另一个答案的建议,使用 init(),但这缺点是即使没有运行测试用例也会完成设置(可能是因为您使用 go test -run <regexp> 限制了测试用例。)

我会说,如果您认为您需要在不同测试之间共享设置,这些设置在您确实需要时执行一次,如果您真的需要它,并且如果表驱动测试不会更好。


在测试诸如标志解析器或搅动数字的算法之类的琐碎事物时,这非常有用。但是在尝试测试都需要类似样板代码的各种功能时,它并没有真正的帮助。我想我可以在一个数组中定义我的测试函数并对其进行迭代,但是它并不是真正的表驱动,而是一个简单的循环,它应该只是构建到测试框架本身中(以适当的测试套件的形式)具有设置/拆卸功能)
H
HairOfTheDog

使用以下模板,您可以在执行设置和拆卸的每个 TestMethod 中进行单行调用。

func setupTest() func() {
    // Setup code here

    // tear down later
    return func() {
        // tear-down code here
    }
}

func TestMethod(t *testing.T) {
    defer setupTest()()
    // asserts, ensures, requires... here
}

j
jknair0

如果有人正在寻找@BeforeEach(在测试文件中的每个测试之前运行)和@AfterEach(在测试文件中的测试之后运行)的替代方案,这里有一个帮助程序片段。

func CreateForEach(setUp func(), tearDown func()) func(func()) {
    return func(testFunc func()) {
        setUp()
        testFunc()
        tearDown()
    }
}

您可以在 TestMain 的帮助下像下面这样使用它

var RunTest = CreateForEach(setUp, tearDown)

func setUp() {
   // SETUP METHOD WHICH IS REQUIRED TO RUN FOR EACH TEST METHOD
   // your code here
}

func tearDown() {
  // TEAR DOWN METHOD WHICH IS REQUIRED TO RUN FOR EACH TEST METHOD
  // your code here
}

fun TestSample(t *testing.T) {
  RunTest(func() {
    // YOUR CODE HERE
  })
}

您也可以检查:go-beforeeach


h
houqp

无耻的插件,我创建了 https://github.com/houqp/gtest 来帮助解决这个问题。

这是一个简单的例子:

import (
  "strings"
  "testing"
  "github.com/houqp/gtest"
)

type SampleTests struct{}

// Setup and Teardown are invoked per test group run
func (s *SampleTests) Setup(t *testing.T)      {}
func (s *SampleTests) Teardown(t *testing.T)   {}
// BeforeEach and AfterEach are invoked per test run
func (s *SampleTests) BeforeEach(t *testing.T) {}
func (s *SampleTests) AfterEach(t *testing.T)  {}

func (s *SampleTests) SubTestCompare(t *testing.T) {
  if 1 != 1 {
    t.FailNow()
  }
}

func (s *SampleTests) SubTestCheckPrefix(t *testing.T) {
  if !strings.HasPrefix("abc", "ab") {
    t.FailNow()
  }
}

func TestSampleTests(t *testing.T) {
  gtest.RunSubTests(t, &SampleTests{})
}

您可以在一个包中创建任何您想要的测试组,每个测试组使用一组不同的设置/拆卸例程。


S
Saurabh

您可以将 testing package 用于测试 setup - 将为所有测试设置阶段,而 teardown - 将在测试运行后清理阶段。

下面计算矩形的面积:

package main

import (
    "errors"
    "fmt"
)

func area(height float64, width float64) (float64, error) {
    if height == width {
        fmt.Printf("Rectangle with given dimension is a square. Area is: %f\n", height * width)
        return height * width, nil
    } else if height <= 0 || width <= 0 {
        return 0, errors.New("Both dimensions need to be positive")
    } else {
        fmt.Printf("Area is: %f\n", height * width)
        return height * width, nil
    }
}

func main() {
    var length float64  = 4.0
    var breadth float64 = 5.0
    area(length, breadth)
}

这是使用 TestMain 作为 Salvador Dali 的 explains 进行测试设置和拆卸的实现。 (注意,从 v1.15 开始,不再需要 TestMain 函数来调用 os.Exit [ref])

package main

import (
    "log"
    "testing"
)

var length float64
var breadth float64

func TestMain(m *testing.M) {
    setup()
    m.Run() 
    teardown()
}

func setup() {
    length = 2.0
    breadth = 3.0
    log.Println("\n-----Setup complete-----")
}

func teardown() {
    length = 0
    breadth = 0
    log.Println("\n----Teardown complete----")
}

func TestAreaOfRectangle(t *testing.T) {
    val, err := area(length, breadth)   
    want := 6.0

    if val != want && err != nil {
        t.Errorf("Got %f and %v; Want %f and %v", val, err, want, nil)
    }
}

这是使用子测试进行测试设置和拆卸的实现:

package main

import "testing"

func TestInvalidRectangle(t *testing.T) {
    // setup
    var length float64 = -2.0
    var breadth float64 = 3.0
    t.Log("\n-----Setup complete for invalid rectangle-----")

    // sub-tests
    t.Run("invalid dimensions return value", func(t *testing.T) {
        val, _ := area(length, breadth)
        area := 0.0

        if val != area {
            t.Errorf("Got %f; Want %f", val, area)
        }
    })

    t.Run("invalid dimensions message", func(t *testing.T) {
        _, err := area(length, breadth)
        want := "Both dimensions need to be positive"

        if err.Error() != want {
            t.Errorf("Got error: %v; Want error: %v", err.Error(), want)
        }
    })

    // teardown
    t.Cleanup(func(){
        length = 0
        breadth = 0
        t.Log("\n----Teardown complete for invalid rectangle----")
    })
}

func TestRectangleIsSquare(t *testing.T) {
    var length float64 = 3.0
    var breadth float64 = 3.0
    t.Log("\n-----Rectangle is square setup complete-----")

    t.Run("valid dimensions value and message", func(t *testing.T) {
        val, msg := area(length, breadth)
        area := 9.0
        if val != area && msg != nil {
            t.Errorf("Got %f and %v; Want %f and %v", val, msg, area, nil)
        }
    })

    t.Cleanup(func(){
        length = 0
        breadth = 0
        t.Log("\n----Rectangle is square teardown Complete----")
    })
}