【问题标题】:How to Mock only specific method in Golang如何在 Golang 中仅模拟特定方法
【发布时间】:2017-08-07 22:45:45
【问题描述】:

我对 golang 还很陌生,我正在为一项简单的任务而苦苦挣扎。

我在 golang 中有以下课程

type struct A {
}

func (s *A) GetFirst() {
    s.getSecond()
}

func (s *A) getSecond() {
    // do something
}

我想为它写一些测试,但是我需要覆盖getSecond()。我尝试在我的测试文件中执行以下操作

type Ai interface {
    getSecond()
}

type testA struct {
    A
}

func (s *testA) getSecond() {
     // do nothing
}

func TestA(t *testing.T) {
   a := &A{}
   t := &testA{A: a}
   t.GetFirst()  
}

这里的想法是将 A getSecond() 方法暴露给接口并通过使用嵌入来覆盖,但这似乎不起作用。测试仍然调用getSecond() 的原始实现,而不是我模拟的。

一种解决方案当然是为 A 创建一个包含getFirst()getSecond() 的适当接口,然后在测试中创建一个实现getFirst() 调用原始实现和getSecond() 一个假人的结构。我觉得这很麻烦,而且不是正确的做事方式。

另一种可能性是在实际实现中将getSecond() 分配给一个变量并在测试中覆盖该变量,但我也觉得这样做只是为了一个简单的覆盖有点奇怪。

我错了吗?有没有更简单的方法可以使用 golang 做到这一点?

【问题讨论】:

  • 为什么要模拟这两种方法?真正的原因是什么?真正有效的是:构造一个A(这很容易,因为您可以访问所有血腥的内部结构),然后检查getFirst()getSecond() 是否产生所需的结果。冲洗并重复。看看标准库是如何做到的。也许你还在受其他语言的影响,getFirst() 使用了getSecond(),这让你相信你必须模拟getSecond()。不,您可以通过A 的状态来控制getSecond() 所做的事情。请注意:Go 中没有 getter(而不是 golang)。
  • 实际上那些不是吸气剂,我只是选择像示例中那样命名这些函数:) 这里的问题是 getFirst 将调用 getSecond 执行在单元测试下不起作用的东西情况。
  • 更准确地说。 A 有一个 baseUrl 并且 getFirst() 执行一个 http 请求,但是在此之前它需要根据请求的类型重写 url 基域以包含子域,这就是 getSecond() 所做的。这会导致 httptest 包出现问题,因为您最终会得到 subdomain.127.0.0.1
  • 好吧,然后将 getFirst() 分成两部分 X()Y()Y 部分是对 URL 执行 HTTP 请求的主要操作,该 URL 是 Y(url *url.URL) 的参数或 A 的字段。现在您可以测试整个“获取一些 URL 并对其进行处理”是否有效。 X 部分调用 getSecond() 并负责生成的 URL。 getFirst() 现在所做的只不过是将 X 和 Y 链接起来,也许将 Y 的输出填充到 X 中。如果 Y 工作正常(可测试)并且 X 工作(也可测试)你就完成了。
  • 你说得对,它可以解决大部分问题,但是测试getFirst() 以防止某些未来的开发人员进入并添加测试未涵盖的逻辑呢?能够通过测试捕捉到这样的场景会很好

标签: unit-testing go interface overriding


【解决方案1】:

按照this answer,您不能真正覆盖 golang 中的方法。但是,正如您所指出的,您可以为“getSecond 方法”提供一个单独的接口,并在您的测试用例中有一个实现,在您的实际代码中有一个实现。

type s interface{ 
    getSecond()
}

type A struct{
    s
}

type a struct{

}

func (s *A) GetFirst() {
    s.getSecond()
}

func (s a) getSecond() {
    // do something
}

//Use a 
A{a{}}

然后在测试中有一个不同的'a'实现

type ta struct {

}
func (s ta) getSecond() {
    // do nothing
}

A{ta{}}

【讨论】:

  • 鉴于我在问题中提供的数据量,我认为这是一个非常可接受的解决方案。如果您可以忍受不测试一种用于粘合整个事物并且不为其添加额外逻辑的方法,那么 Volker 在我的问题下方的 cmets 中发布的内容也是如此。
  • 如果您想在getSecond 中模拟getThird 函数,这个解决方案会变得很麻烦!
【解决方案2】:

我认为至少有两种选择。

始终使用函数字段

当模拟的函数与我正在模拟的结构无关时,可能是第三方函数,或者从我的应用程序的另一个组件编写的东西。 我会在初始化服务时分配真正工作的,或者在测试中分配模拟的。

// service.go
type MyService struct {
    getRandomID func() string
}

type Car struct {
    ID   string
    Name string
}

func (s *MyService) NewCar() (*Car, error) {
    car := Car{
        ID:   s.getRandomID(),
        Name: "ThisCar",
    }

    return &car, nil
}


// service_test.go

func newIDsForTests() func() string {
    i := 0
    return func() string {
        i++
        return fmt.Sprintf("%024d", i)
    }
}

func TestNewCar(t *testing.T) {
    s := MyService{
        getRandomID: newIDsForTests(),
    }

    actual, err := s.NewCar()

    if err != nil {
        panic(err)
    }

    expected := Car{ID: "000000000000000000000001", Name: "ThisCar"}
    if *actual != expected {
        panic("cars don't match")
    }
}


The Go Playground working example

仅在模拟时使用函数字段

当要模拟的函数与我正在使用的结构真正相关时,这是该组件的一部分。 我将始终使用真正的工作,并在测试需要时分配一个模拟函数。

虽然我认为这个解决方案相当丑陋,但我也认为它肯定易于使用和维护,同时还可以让您对代码进行 100% 的单元测试!

我的想法是在结构中添加一个字段mockedGetSecond,并仅在要模拟真实getSecond 的测试中设置其值。当然,在实际实现中,您必须添加一个检查,如果此 func 不是 nil,则必须使用它。

这可能不是一个好的模式,或者我想经常使用的东西,但我想我会用它来模拟一个执行大量逻辑的函数(以及大量的数据库调用,并且需要各种input, ...) 并且经常在同一服务的函数中调用。

// service.go

import (
    "fmt"
    "testing"
)

type MyService struct {
    mockedGetSecond func() (string, error)
}

func (s *MyService) GetFirst() error {
    secondVal, err := s.getSecond()
    if err != nil {
        return err
    }

    fmt.Println("getSecond returned: ", secondVal)

    return nil
}

func (s *MyService) getSecond() (string, error) {
    if s.mockedGetSecond != nil {
        return s.mockedGetSecond()
    }

    // very complex function

    return "real", nil
}

// service_test.go

func TestGetFirst(t *testing.T) {
    myService := MyService{
        mockedGetSecond: func() (string, error) {
            return "mocked", nil
        },
    }

    err := myService.GetFirst()
    if err != nil {
        panic(err)
    }
}

func TestGetSecond(t *testing.T) {
    myService := MyService{}

    actual, err := myService.getSecond()
    if err != nil {
        panic(err)
    }

    if actual != "real" {
        panic("I would expect 'real'")
    }
}

The Go Playground working example

=== RUN   TestGetFirst
getSecond returned:  mocked
--- PASS: TestGetFirst (0.00s)
=== RUN   TestGetSecond
--- PASS: TestGetSecond (0.00s)
PASS

【讨论】:

    【解决方案3】:

    mockcompose (https://github.com/kelveny/mockcompose) 是我为解决这个问题而创建的。

    Go 中的类不是一等公民,对于那些来自其他语言世界(即 Java)的人来说,缺乏模拟兄弟方法的能力有时会很麻烦。

    假设你有一个类 foo

    package foo
    
    type foo struct {
        name string
    }
    
    func (f *foo) Foo() string {
        if f.Bar() {
            return "Overriden with Bar"
        }
    
        return f.name
    }
    
    func (f *foo) Bar() bool {
        if f.name == "bar" {
            return true
        }
    
        return false
    }
    

    您想测试 Foo() 方法,但是,当 Foo() 调用 Bar() 时,您希望 Bar() 被模拟。

    使用mockcompose,你可以先配置go generate,然后让mockcompose为你生成管道东西。

    mocks.go

    //go:generate mockcompose -n testFoo -c foo -real Foo -mock Bar
    package foo
    

    mockcompose 然后会生成代码:mockc_testFoo_test.go

    //
    // CODE GENERATED AUTOMATICALLY WITH github.com/kelveny/mockcompose
    // THIS FILE SHOULD NOT BE EDITED BY HAND
    //
    package foo
    
    import (
        "github.com/stretchr/testify/mock"
    )
    
    type testFoo struct {
        foo
        mock.Mock
    }
    
    func (f *testFoo) Foo() string {
        if f.Bar() {
            return "Overriden with Bar"
        }
        return f.name
    }
    
    func (m *testFoo) Bar() bool {
    
        _mc_ret := m.Called()
    
        var _r0 bool
    
        if _rfn, ok := _mc_ret.Get(0).(func() bool); ok {
            _r0 = _rfn()
        } else {
            if _mc_ret.Get(0) != nil {
                _r0 = _mc_ret.Get(0).(bool)
            }
        }
    
        return _r0
    
    }
    

    现在,您只需像使用其他语言(即 Java)一样编写单元测试逻辑,例如:

    func TestFoo(t *testing.T) {
        assert := require.New(t)
    
        fooObj := &testFoo{}
    
        // Mock sibling method Bar()
        fooObj.On("Bar").Return(false)
    
        s := fooObj.Foo()
        assert.True(s == "")
    }
    

    详情请查看https://github.com/kelveny/mockcomposemockcompose 还可以通过标注从其他包中导入的函数来帮助您测试常规函数。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2019-03-13
      • 1970-01-01
      • 2018-05-04
      • 2019-12-07
      • 1970-01-01
      相关资源
      最近更新 更多