Go 中的模拟函数

我对依赖性感到困惑。我希望能够用模拟函数替换一些函数调用。下面是我的代码片段:

func get_page(url string) string {
get_dl_slot(url)
defer free_dl_slot(url)
    

resp, err := http.Get(url)
if err != nil { return "" }
defer resp.Body.Close()
    

contents, err := ioutil.ReadAll(resp.Body)
if err != nil { return "" }
return string(contents)
}


func downloader() {
dl_slots = make(chan bool, DL_SLOT_AMOUNT) // Init the download slot semaphore
content := get_page(BASE_URL)
links_regexp := regexp.MustCompile(LIST_LINK_REGEXP)
matches := links_regexp.FindAllStringSubmatch(content, -1)
for _, match := range matches{
go serie_dl(match[1], match[2])
}
}

我希望能够测试 downloader(),而不需要通过 http 实际获取页面-即通过模拟 get_page(更容易,因为它只返回作为字符串的页面内容)或 http.Get()

我发现 这根线似乎是一个类似的问题。朱利安菲利普斯提出了他的库,威斯莫克作为一个解决方案,但我无法得到它的工作。下面是我的测试代码的相关部分,坦白地说,这些代码对我来说很大程度上是货物狂热代码:

import (
"testing"
"net/http" // mock
"code.google.com/p/gomock"
)
...
func TestDownloader (t *testing.T) {
ctrl := gomock.NewController()
defer ctrl.Finish()
http.MOCK().SetController(ctrl)
http.EXPECT().Get(BASE_URL)
downloader()
// The rest to be written
}

测试结果如下:

错误: 未能安装’_ et/http’: 退出状态1输出: 无法加载 Package: package _ et/http: found package http (chunked.go) and main (main _ mock. go)
/var/files/z9/ql _ yn5h550s6shtb9c5sggj40000gn/T/withmock 570825607/path/src/_ et/http

Withmock 是我测试问题的解决方案吗? 我应该怎么做才能让它工作?

160859 次浏览

就我个人而言,我不使用 gomock(或者任何相关的模仿框架; 没有它,在 Go 中模仿很容易)。我要么将一个依赖项作为参数传递给 downloader()函数,要么将 downloader()作为一个类型的方法,该类型可以保存 get_page依赖项:

方法1: 传递 get_page()作为 downloader()的参数

type PageGetter func(url string) string


func downloader(pageGetterFunc PageGetter) {
// ...
content := pageGetterFunc(BASE_URL)
// ...
}

主要内容:

func get_page(url string) string { /* ... */ }


func main() {
downloader(get_page)
}

测试:

func mock_get_page(url string) string {
// mock your 'get_page()' function here
}


func TestDownloader(t *testing.T) {
downloader(mock_get_page)
}

方法2: 使 download()成为类型为 Downloader的方法:

如果不想将依赖项作为参数传递,也可以将 get_page()作为类型的成员,并将 download()作为该类型的方法,然后该方法可以使用 get_page:

type PageGetter func(url string) string


type Downloader struct {
get_page PageGetter
}


func NewDownloader(pg PageGetter) *Downloader {
return &Downloader{get_page: pg}
}


func (d *Downloader) download() {
//...
content := d.get_page(BASE_URL)
//...
}

主要内容:

func get_page(url string) string { /* ... */ }


func main() {
d := NewDownloader(get_page)
d.download()
}

测试:

func mock_get_page(url string) string {
// mock your 'get_page()' function here
}


func TestDownloader() {
d := NewDownloader(mock_get_page)
d.download()
}

如果更改函数定义以使用变量:

var get_page = func(url string) string {
...
}

您可以在测试中覆盖它:

func TestDownloader(t *testing.T) {
get_page = func(url string) string {
if url != "expected" {
t.Fatal("good message")
}
return "something"
}
downloader()
}

不过要小心,如果其他测试测试您覆盖的函数的功能,那么它们可能会失败!

Go 的作者在 Go 标准库中使用这个模式将测试挂钩插入到代码中,以使事情更容易测试:

我会这么做,

总部

var getPage = get_page
func get_page (...


func downloader() {
dl_slots = make(chan bool, DL_SLOT_AMOUNT) // Init the download slot semaphore
content := getPage(BASE_URL)
links_regexp := regexp.MustCompile(LIST_LINK_REGEXP)
matches := links_regexp.FindAllStringSubmatch(content, -1)
for _, match := range matches{
go serie_dl(match[1], match[2])
}
}

测试

func TestDownloader (t *testing.T) {
origGetPage := getPage
getPage = mock_get_page
defer func() {getPage = origGatePage}()
// The rest to be written
}


// define mock_get_page and rest of the codes
func mock_get_page (....

我会避免在戈兰 _最好用骆驼箱

我使用的方法略有不同,其中 公众人士结构方法实现 接口,但它们的逻辑仅限于包装以这些 接口为参数的 二等兵(未导出)函数。这使您可以模拟实际上任何依赖项,并且在测试套件之外使用一个干净的 API,从而获得所需的粒度。

为了理解这一点,必须理解 您可以访问测试用例中未导出的方法(即从 _test.go文件中) ,以便测试这些文件,而不是测试导出的那些除了包装之外没有逻辑的文件。

总结: 测试未导出的函数,而不是测试导出的函数!

举个例子,假设我们有一个 Slack API 结构,它有两个方法:

  • 向 Slack webhook 发送 HTTP 请求的 SendMessage方法
  • 给定一个字符串片段的 SendDataSynchronously方法迭代这些字符串,并在每次迭代时调用 SendMessage

因此,为了在不发出 HTTP 请求的情况下测试 SendDataSynchronously,我们每次都必须模拟 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 上查看完整的例子,这里是 放松,走文件,这里是 松弛测试,开始文件。

还有 给你

考虑到单元测试是这个问题的领域,强烈建议您使用 猴子。此包使您可以在不更改原始源代码的情况下进行模拟测试。与其他答案相比,这个答案更“无干扰”。

总台

type AA struct {
//...
}
func (a *AA) OriginalFunc() {
//...
}

模拟考试

var a *AA


func NewFunc(a *AA) {
//...
}


monkey.PatchMethod(reflect.TypeOf(a), "OriginalFunc", NewFunc)

坏的一面是:

  • 由 Dave.C 提醒,此方法不安全,所以不要在单元测试之外使用它。
  • 是不成语的。

好的一面是:

  • 是非侵入性的。让你做的事情没有改变主代码。就像托马斯说的。
  • 使用最少的代码更改包的行为(可能由第三方提供)。

警告: 这可能会使可执行文件的大小增加一点,并且会降低一点运行时性能。IMO,这将是更好的,如果 golang 有这样的功能,如宏或函数修饰器。

如果你想在不改变 API 的情况下模拟函数,最简单的方法就是稍微改变一下实现:

func getPage(url string) string {
if GetPageMock != nil {
return GetPageMock()
}


// getPage real implementation goes here!
}


func downloader() {
if GetPageMock != nil {
return GetPageMock()
}


// getPage real implementation goes here!
}


var GetPageMock func(url string) string = nil
var DownloaderMock func() = nil

通过这种方式,我们实际上可以从其他函数中模拟一个函数。为了更方便,我们可以提供这样的模仿样板:

// download.go
func getPage(url string) string {
if m.GetPageMock != nil {
return m.GetPageMock()
}


// getPage real implementation goes here!
}


func downloader() {
if m.GetPageMock != nil {
return m.GetPageMock()
}


// getPage real implementation goes here!
}


type MockHandler struct {
GetPage func(url string) string
Downloader func()
}


var m *MockHandler = new(MockHandler)


func Mock(handler *MockHandler) {
m = handler
}

在测试文件中:

// download_test.go
func GetPageMock(url string) string {
// ...
}


func TestDownloader(t *testing.T) {
Mock(&MockHandler{
GetPage: GetPageMock,
})


// Test implementation goes here!


Mock(new(MockHandler)) // Reset mocked functions
}

最简单的方法是将函数设置为全局变量,并在测试设置自定义方法之前设置函数

// package base36


func GenerateRandomString(length int) string {
// your real code
}




// package teamManager


var RandomStringGenerator = base36.GenerateRandomString


func (m *TeamManagerService) CreateTeam(ctx context.Context) {
 

// we are using the global variable
code = RandomStringGenerator(5)
 

// your application logic


return  nil
}

在您的测试中,您必须首先模拟该全局变量

    teamManager.RandomStringGenerator = func(length int) string {
return "some string"
}
    

service := &teamManager.TeamManagerService{}
service.CreateTeam(context.Background())
// now when we call any method that user teamManager.RandomStringGenerator, it will call our mocked method

另一种方法是将 RRandom StringGenerator 作为一个依赖项传递给它,并将其存储在 TeamManagerService中,然后像下面这样使用它:

// package teamManager


type TeamManagerService struct {
RandomStringGenerator func(length int) string
}


// in this way you don't need to change your main/where this code is used
func NewTeamManagerService() *TeamManagerService {
return &TeamManagerService{RandomStringGenerator: base36.GenerateRandomString}
}


func (m *TeamManagerService) CreateTeam(ctx context.Context) {
 

// we are using the struct field variable
code = m.RandomStringGenerator(5)
 

// your application logic


return  nil
}

在您的测试中,您可以使用您自己的自定义函数

    myGenerator = func(length int) string {
return "some string"
}
    

service := &teamManager.TeamManagerService{RandomStringGenerator: myGenerator}
service.CreateTeam(context.Background())

你像我一样使用证词: D 你可以做到的

// this is the mock version of the base36 file
package base36_mock


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


var Mock = mock.Mock{}


func GenerateRandomString(length int) string {
args := Mock.Called(length)
return args.String(0)
}

在您的测试中,您可以使用您自己的自定义函数

   base36_mock.Mock.On("GenerateRandomString", 5).Return("my expmle code for this test").Once()
    

service := &teamManager.TeamManagerService{RandomStringGenerator: base36_mock.GenerateRandomString}
service.CreateTeam(context.Background())

我也有过类似的经历。我尝试为一个函数编写 unitTest,这个函数有许多客户端调用它。让我提出我探讨过的两个选择。其中一个已经在这个帖子中讨论过了,为了人们的搜索,我将不管不顾地重复它。

方法1: 将要模拟的函数声明为全局变量


一种选择是声明一个全局变量(有一些陷阱)。

例如:

package abc


var getFunction func(s string) (string, error) := http.Get


func get_page(url string) string {
....
resp, err := getFunction(url)
....
}


func downloader() {
.....
}

测试功能如下:

package abc


func testFunction(t *testing.T) {
actualFunction := getFunction
getFunction := func(s string) (string, error) {
//mock implementation
}
defer getFunction = actualFunction
.....
//your test
......
}


注意: 测试和实际实现在同一个包中。

用上面的方法思考有一些限制!

  1. 由于竞态条件的风险,不可能进行并行测试。
  2. 通过使函数成为一个变量,我们引入了一个小的风险,即引用被未来在同一个包中工作的开发人员修改。

方法2: 创建一个包装函数


另一种方法是将希望模拟的方法作为参数传递给函数,以启用可测试性。在我的例子中,我已经有许多客户调用这个方法,因此,我想避免违反现有的契约。所以,我最终创建了一个包装函数。

例如:

package abc


type getOperation func(s string) (string, error)


func get_page(url string, op getOperation) string {
....
resp, err := op(url)
....
}


//contains only 2 lines of code
func downloader(get httpGet) {
op := http.Get
content := wrappedDownloader(get, op)
}


//wraps all the logic that was initially in downloader()
func wrappedDownloader(get httpGet, op getOperation) {
....
content := get_page(BASE_URL, op)
....
}

现在为了测试实际的逻辑,您将测试对 wrappedDownloader的调用,而不是对 Downloader的调用,并且您将向它传递一个模拟的 getOperation。这允许您测试所有的业务逻辑,同时不违反与方法的当前客户端的 API 协议。