跳转到主要内容

🚀 我的新课程 - Golang 测试圣经现已推出,涵盖了启动和运行为 Go 应用程序创建测试所需的一切!

断言是我真正觉得 Go 中的标准库缺少的东西。您绝对可以通过 if 比较之类的方法和其他任何方法获得相同的结果,但这并不是编写测试文件的最简洁方式。

这就是像stretchr/testify 这样的人来拯救这一天的地方。这个包已经迅速成为最流行的测试包之一,如果不是全球 Go 开发人员最流行的测试包的话。

它优雅的语法允许您编写非常简单且有意义的断言。

入门


为了启动和运行 testify 包,我们必须做的第一件事就是安装它。现在,如果您使用的是 Go 模块,那么这只是在您的一个 *_test.go 文件的顶部导入包之后调用 go test ... 的情况。

但是,如果您仍然停留在旧版本的 Go 上,您可以通过键入以下内容来获取此包:

go get github.com/stretchr/testify


完成此操作后,我们应该开始将其合并到我们的各种测试套件中。

一个简单的例子


让我们从传统上如何用 Go 编写测试开始。这应该让我们很好地了解 testify 在提高可读性方面带来了什么。

我们将从定义一个非常简单的 Go 程序开始,该程序具有一个导出函数 Calculate()。

main.go

package main

import (
    "fmt"
)

// Calculate returns x + 2.
func Calculate(x int) (result int) {
    result = x + 2
    return result
}

func main() {
    fmt.Println("Hello World")
}

如果我们要使用传统方法为此编写测试,我们通常会得到这样的结果:

main_test.go

package main

import (
    "testing"
)

func TestCalculate(t *testing.T) {
    if Calculate(2) != 4 {
        t.Error("Expected 2 + 2 to equal 4")
    }
}

然后我们可以通过调用 go test ./... -v 来尝试运行这个简单的测试,传入 -v 标志以确保我们可以看到更详细的输出。

如果我们想要更高级一点,我们可能会在此处合并表驱动测试,以确保测试各种案例。不过现在,让我们尝试修改这个基本方法,看看 testify 是如何工作的:

main_test.go

package main

import (
    "testing"
    "github.com/stretchr/testify/assert"
)

func TestCalculate(t *testing.T) {
  assert.Equal(t, Calculate(2), 4)
}

太棒了,如您所见,我们已经成功地使用 assert.Equal 函数简洁地测试了相等性。这看起来像是一种改进,因为我们需要阅读的代码行数更少,而且我们可以清楚地看到测试函数试图实现的目标。

否定测试用例和 Nil 测试


所以,我们已经研究了快乐路径测试,但是否定断言和 Nil 检查怎么样。好吧,谢天谢地,testify 包有允许我们测试两者的方法。

假设我们想测试一个返回给定应用程序状态的函数。例如,如果应用程序处于活动状态并正在等待请求,则状态将返回“等待”,如果它已崩溃,则它将返回“关闭”以及各种其他状态,用于处理请求或何时它正在等待第三方等。

当我们执行测试时,只要状态不等于“down”,我们就希望测试通过,因此我们可以在这个特定的假设情况下使用 assert.NotEqual()。

func TestStatusNotDown(t *testing.T) {
  assert.NotEqual(t, status, "down")
}


如果我们想测试“status”是否不是 nil,那么我们可以使用 assert.Nil(status) 或 assert.NotNil(object),这取决于我们希望如何对它是 nil 做出反应。

将 Testify 与表驱动测试相结合


将 testify 合并到我们的测试套件中并不一定会阻止我们使用诸如表驱动测试之类的方法,事实上,它使测试变得更简单。

main_test.go

package main

import (
    "testing"

    "github.com/stretchr/testify/assert"
)

func TestCalculate(t *testing.T) {
    assert := assert.New(t)

    var tests = []struct {
        input    int
        expected int
    }{
        {2, 4},
        {-1, 1},
        {0, 2},
        {-5, -3},
        {99999, 100001},
    }

    for _, test := range tests {
        assert.Equal(Calculate(test.input), test.expected)
    }
}

注意我们在这个例子中调用 assert.Equal() 的方式与前面的例子略有不同。我们已经使用 assert.New(t) 初始化了 assert,现在我们可以多次调用 assert.Equal(),只需传入输入和预期值,而不是每次都必须将 t 作为第一个参数传入.这没什么大不了的,但它肯定有助于使我们的测试看起来更干净。

模仿(Mocking)


testify 包的另一个出色功能是它的模拟功能。模拟有效地允许我们编写替换对象来模拟我们代码中某些对象的行为,我们不一定希望在每次运行测试套件时触发这些行为。

例如,这可能是一个消息服务或电子邮件服务,当它被调用时,它会向客户发送电子邮件。如果我们正在积极开发我们的代码库,我们可能每天要运行数百次测试,并且我们可能不想每天向客户发送数百封电子邮件和/或消息,因为他们可能会开始感到不满。

那么,我们如何使用 testify 包进行模拟呢?

一个模拟的例子


让我们通过一个相当简单的例子来看看如何使用模拟。在这个例子中,我们有一个系统会尝试向客户收取产品或服务的费用。当调用此 ChargeCustomer() 方法时,它将随后调用消息服务,该消息服务将向客户发送 SMS 文本消息,告知他们已收取的金额。

main.go

package main

import (
    "fmt"
)

// MessageService handles notifying clients they have
// been charged
type MessageService interface {
    SendChargeNotification(int) error
}

// SMSService is our implementation of MessageService
type SMSService struct{}

// MyService uses the MessageService to notify clients
type MyService struct {
    messageService MessageService
}

// SendChargeNotification notifies clients they have been
// charged via SMS
// This is the method we are going to mock
func (sms SMSService) SendChargeNotification(value int) error {
    fmt.Println("Sending Production Charge Notification")
    return nil
}

// ChargeCustomer performs the charge to the customer
// In a real system we would maybe mock this as well
// but here, I want to make some money every time I run my tests
func (a MyService) ChargeCustomer(value int) error {
    a.messageService.SendChargeNotification(value)
    fmt.Printf("Charging Customer For the value of %d\n", value)
    return nil
}

// A "Production" Example
func main() {
    fmt.Println("Hello World")

    smsService := SMSService{}
    myService := MyService{smsService}
    myService.ChargeCustomer(100)
}

那么,我们如何进行测试以确保我们不会让客户发疯?好吧,我们通过创建一个名为 smsServiceMock 的新结构体来模拟我们的 SMSService,并将 mock.Mock 添加到它的字段列表中。

然后,我们将 SendChargeNotification 方法存根,这样它就不会真正向我们的客户端发送通知并返回 nil 错误。

最后,我们创建了 TestChargeCustomer 测试函数,该函数依次实例化一个 smsServiceMock 类型的新实例,并指定调用 SendChargeNotification 时应该发生什么。

main_test.go

package main

import (
    "fmt"
    "testing"

    "github.com/stretchr/testify/mock"
)

// smsServiceMock
type smsServiceMock struct {
    mock.Mock
}

// Our mocked smsService method
func (m *smsServiceMock) SendChargeNotification(value int) bool {
    fmt.Println("Mocked charge notification function")
    fmt.Printf("Value passed in: %d\n", value)
  // this records that the method was called and passes in the value
  // it was called with
  args := m.Called(value)
  // it then returns whatever we tell it to return
  // in this case true to simulate an SMS Service Notification
  // sent out
    return args.Bool(0)
}

// we need to satisfy our MessageService interface
// which sadly means we have to stub out every method
// defined in that interface
func (m *smsServiceMock) DummyFunc() {
    fmt.Println("Dummy")
}

// TestChargeCustomer is where the magic happens
// here we create our SMSService mock
func TestChargeCustomer(t *testing.T) {
    smsService := new(smsServiceMock)

  // we then define what should be returned from SendChargeNotification
  // when we pass in the value 100 to it. In this case, we want to return
  // true as it was successful in sending a notification
    smsService.On("SendChargeNotification", 100).Return(true)

  // next we want to define the service we wish to test
  myService := MyService{smsService}
  // and call said method
    myService.ChargeCustomer(100)

  // at the end, we verify that our myService.ChargeCustomer
  // method called our mocked SendChargeNotification method
    smsService.AssertExpectations(t)
}

因此,当我们运行这个调用 go test ./... -v 时,我们应该看到以下输出:

go test ./... -v
=== RUN   TestChargeCustomer
Mocked charge notification function
Value passed in: 100
Charging Customer For the value of 100
--- PASS: TestChargeCustomer (0.00s)
    main_test.go:33: PASS:      SendChargeNotification(int)
PASS
ok      _/Users/elliot/Documents/Projects/tutorials/golang/go-testify-tutorial  0.012s


正如你所看到的,我们的模拟方法被调用而不是我们的“生产”方法,我们已经能够验证我们的 myService.ChargeCustomer() 方法是否按照我们期望的方式运行!

快乐的日子,我们现在已经能够使用模拟完全测试一个更复杂的项目。值得注意的是,这种技术可以用于各种不同的系统,例如模拟数据库查询或与其他 API 交互的方式。总的来说,模拟是非常强大的东西,如果你要在 Go 中测试生产级系统,绝对是你应该尝试掌握的东西。

用Mocking生成模拟


因此,在上面的示例中,我们自己模拟了所有各种方法,但在现实生活中的示例中,这可能代表了许多不同的方法和函数要模拟的地狱。

值得庆幸的是,这就是 vektra/mockery 包提供给我们的助手的地方。

mocky 二进制文件可以使用你在 Go 包中定义的任何接口的名称,它会自动将生成的 mock 输出到 mocks/InterfaceName.go。当您想节省大量时间时,这非常方便,我强烈建议您检查一下!

关键要点

 

  • Testify 帮助您简化在测试用例中编写断言的方式。
  • Testify 还可用于在测试框架中模拟对象,以确保您在测试时不会调用生产端点。

结论


希望这有助于揭开使用stretchr/testify 包测试Go 项目的神秘面纱。在本教程中,我们设法了解如何使用 testify 包中的断言来执行诸如断言事物是否相等、不相等或 nil 之类的事情。

我们还能够研究如何模拟系统的各个部分,以确保在运行测试时,您不会随后开始与生产系统交互并做一些您不太想做的事情。

如果您觉得这很有用,或者您有任何意见或反馈,请随时在下面的评论部分告诉我。

文章链接