我使用了一种稍微不同的方法,其中公共结构方法实现接口,但它们的逻辑仅限于包装将这些接口作为参数的私有(未导出)函数。这为您提供了模拟几乎任何依赖项所需的粒度,并且有一个干净的 API 可以在您的测试套件之外使用。
要理解这一点,必须了解您可以访问测试用例中未导出的方法(即从您的_test.go
文件中),因此您可以测试这些方法,而不是测试除了包装之外没有任何逻辑的导出方法。
总结一下:测试未导出的函数而不是测试导出的函数!
让我们举个例子。假设我们有一个 Slack API 结构,它有两种方法:
- 向
SendMessage
Slack webhook 发送 HTTP 请求的方法
- 给定字符串切片的
SendDataSynchronously
方法迭代它们并调用SendMessage
每次迭代
因此,为了在SendDataSynchronously
每次不发出 HTTP 请求的情况下进行测试,我们必须模拟SendMessage
,对吗?
package main
import (
"fmt"
)
// URI interface
type URI interface {
GetURL() string
}
// MessageSender interface
type MessageSender interface {
SendMessage(message string) error
}
// This one is the "object" that our users will call to use this package functionalities
type API struct {
baseURL string
endpoint string
}
// Here we make API implement implicitly the URI interface
func (api *API) GetURL() string {
return api.baseURL + api.endpoint
}
// Here we make API implement implicitly the MessageSender interface
// Again we're just WRAPPING the sendMessage function here, nothing fancy
func (api *API) SendMessage(message string) error {
return sendMessage(api, message)
}
// We want to test this method but it calls SendMessage which makes a real HTTP request!
// Again we're just WRAPPING the sendDataSynchronously function here, nothing fancy
func (api *API) SendDataSynchronously(data []string) error {
return sendDataSynchronously(api, data)
}
// this would make a real HTTP request
func sendMessage(uri URI, message string) error {
fmt.Println("This function won't get called because we will mock it")
return nil
}
// this is the function we want to test :)
func sendDataSynchronously(sender MessageSender, data []string) error {
for _, text := range data {
err := sender.SendMessage(text)
if err != nil {
return err
}
}
return nil
}
// TEST CASE BELOW
// Here's our mock which just contains some variables that will be filled for running assertions on them later on
type mockedSender struct {
err error
messages []string
}
// We make our mock implement the MessageSender interface so we can test sendDataSynchronously
func (sender *mockedSender) SendMessage(message string) error {
// let's store all received messages for later assertions
sender.messages = append(sender.messages, message)
return sender.err // return error for later assertions
}
func TestSendsAllMessagesSynchronously() {
mockedMessages := make([]string, 0)
sender := mockedSender{nil, mockedMessages}
messagesToSend := []string{"one", "two", "three"}
err := sendDataSynchronously(&sender, messagesToSend)
if err == nil {
fmt.Println("All good here we expect the error to be nil:", err)
}
expectedMessages := fmt.Sprintf("%v", messagesToSend)
actualMessages := fmt.Sprintf("%v", sender.messages)
if expectedMessages == actualMessages {
fmt.Println("Actual messages are as expected:", actualMessages)
}
}
func main() {
TestSendsAllMessagesSynchronously()
}
我喜欢这种方法的地方在于,通过查看未导出的方法,您可以清楚地看到依赖关系是什么。同时,您导出的 API 更简洁,传递的参数更少,因为这里真正的依赖关系只是实现所有这些接口的父接收器。然而,每个函数都可能只依赖于它的一部分(一个,也许是两个接口),这使得重构变得更加容易。很高兴通过查看函数签名来了解您的代码是如何真正耦合的,我认为它是一个强大的防止代码异味的工具。
为方便起见,我将所有内容放入一个文件中,以便您在此处的操场上运行代码,但我建议您也查看 GitHub 上的完整示例,这里是slack.go文件,这里是slack_test.go。
整个事情都在这里。