175

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

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()不通过 h​​ttp 实际获取页面的情况下进行测试 - 即通过模拟get_page(更容易,因为它仅将页面内容作为字符串返回)或http.Get().

我发现这个线程似乎是关于一个类似的问题。Julian Phillips 展示了他的库Withmock作为解决方案,但我无法让它工作。这是我的测试代码的相关部分,老实说,这对我来说主要是货物崇拜代码:

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 输出:无法加载包:包 _et/http:在
/var/folders/z9/中找到包 http (chunked.go) 和 main (main_mock.go) ql_yn5h550s6shtb9c5sggj40000gn/T/withmock570825607/path/src/_et/http

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

4

7 回答 7

232

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

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()
}
于 2013-10-03T20:42:13.607 回答
39

如果您将函数定义更改为使用变量:

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 标准库中使用此模式将测试钩子插入代码中,以使测试更容易:

于 2014-06-24T02:00:23.433 回答
13

我使用了一种稍微不同的方法,其中公共结构方法实现接口,但它们的逻辑仅限于包装将这些接口作为参数的私有(未导出)函数。这为您提供了模拟几乎任何依赖项所需的粒度,并且有一个干净的 API 可以在您的测试套件之外使用。

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

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

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

  • SendMessageSlack 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

整个事情都在这里

于 2018-01-11T11:53:19.050 回答
9

我会做类似的事情,

主要的

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 (....

我会避免_在golang中。更好地使用camelCase

于 2015-05-11T06:55:04.253 回答
1

警告:这可能会稍微增加可执行文件的大小并降低运行时性能。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
}
于 2019-10-01T17:55:18.587 回答
0

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

// 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

另一种方法是将 RandomStringGenerator 作为依赖项传递并将其存储在其中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())
于 2021-12-06T12:56:41.363 回答
-2

考虑到单元测试是这个问题的领域,强烈建议您使用monkey。该软件包使您可以在不更改原始源代码的情况下进行模拟测试。与其他答案相比,它更“非侵入性”。

主要的

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

模拟测试

var a *AA

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

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

不好的一面是:

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

好的一面是:

  • 是非侵入式的。让你在不改变主代码的情况下做事。正如托马斯所说。
  • 让你用最少的代码改变包的行为(可能由第三方提供)。
于 2019-08-08T03:22:35.680 回答