如何在Golang中仅模拟特定的方法

huangapple go评论71阅读模式
英文:

How to Mock only specific method in Golang

问题

我对golang还比较新手,对一个简单的任务感到困惑。

我在golang中有以下的类:

type A struct {
}

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中实现这个?

英文:

I am fairly new to golang and I am struggling with a simple task.

I have the following class in golang

type struct A {
}

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

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

And I want to write some tests for it however for that I need to override getSecond(). I attempted to do the following in my test files

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()  
}

The idea here is to expose A getSecond() method to an interface and override by using embedding however this does not seem to work. The test still calls the original implementation of getSecond() instead of my mocked one.

One solution would of course be to create a proper interface for A which contains getFirst() and getSecond() and then in the test create a struct implementing both where getFirst() call the original implementation and getSecond() a dummy however I feel this is cumbersome and not the correct way of doing things.

Another possibility would be to assign getSecond() in the real implementation to a variable and override the variable in test but I also feel it is a bit strange to do this just for a simple override.

Am I going all wrong about this? Is there any way simpler way to do this using golang?

答案1

得分: 12

你可以在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
}

// 使用a
A{a{}}

然后在测试中使用一个不同的a的实现:

type ta struct {

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

A{ta{}}
英文:

You can't really override methods in golang as per this answer. However, as you point out you can have a separate interface for the "getSecond method" and have one implementation in your test cases and one implementation in your actual code.

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{}}

Then in Test have a different implementation of 'a'

type ta struct {

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

A{ta{}}

答案2

得分: 2

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{}

    // 模拟兄弟方法Bar()
    fooObj.On("Bar").Return(false)

    s := fooObj.Foo()
    assert.True(s == "")
}

详细信息请参阅https://github.com/kelveny/mockcompose,mockcompose还可以帮助你测试调用其他包中导入函数的常规函数。

英文:

mockcompose (https://github.com/kelveny/mockcompose) was created by me to address exactly the problem.

Class in Go is not the first class citizen, for those who come from other language worlds (i.e. Java), lack of ability for mocking sibling methods sometimes bothers.

Say you have a class 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
}

You want to test Foo() method, however, when Foo() calls Bar(), you want Bar() to be mocked.

With mockcompose, you can first configure go generate and let mockcompose generate plumbing things for you.

mocks.go

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

mockcompose will then generate code: 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

}

Now, all you need to do is just to write your unit testing logic as you do in other languages (i.e. Java), for example:

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 == "")
}

For details, please check out https://github.com/kelveny/mockcompose, mockcompose can also help you test regular functions with callouts to imported functions from other packages.

答案3

得分: 1

我认为至少有两种替代方案。

始终使用函数字段

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

// 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")
	}
}


Go Playground 上的可运行示例

仅在模拟时使用函数字段

当要模拟的函数与我正在处理的结构体密切相关,即属于该组件的一部分时,我将始终使用真正的工作函数,并在需要进行测试时分配模拟函数。

虽然我认为这种解决方案相当丑陋,但我也认为它确实易于使用和维护,并且可以让您对代码进行100%的单元测试!

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

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

// 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()
    }

	// 非常复杂的函数

	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'")
	}
}

Go Playground 上的可运行示例

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

I think there are at least two alternatives.

Use always a function field

When the function mocked is something not related to the struct I'm mocking, maybe a third party function, or something written from another component of my application.
I will assign the real working one when initializing the service or the mocked one on the tests.

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

Use a function field only when mocking

When the function to be mocked is something really related to the struct I'm working with, that is part of this component.
I will always use the real working, and assign a mock function when needed for the tests.

While I think this solution is quite ugly, I also think that is for sure easily to use and to maintain, while also let you unit test your code at 100%!

My idea is to add a field mockedGetSecond to the struct, and to set its value only in the tests where you want to mock the real getSecond. In the real implementation you have to add a check of course, that if this func isn't nil, it must be used.

This is probably not a good pattern, or something I would like to use often, but I think I'll will use it to mock a function that do a lot of logic (and a lot of db calls, and need various input, ...) and is often called in the functions in the same service.

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

答案4

得分: 0

如果有人在类似的深入研究中遇到这个问题:我想模拟一个由另一个函数调用的函数,以确保在特定条件下它被调用了x次,就像你在Jest中所做的那样。我希望尽可能保持程序代码与测试无关,但是在接收器方法和接口中重写函数方面进展不大,所以我选择了将函数引用作为参数传递。所以,如果你不是完全固定于面向对象的方法:

// app.go
type SubDoer func(string) string

func SubDo(something string) string {
	...
	return something
}

func DoHandler(somethings []Something) string {
	return Do(somethings, SubDo)
}

func Do(somethings []Something, subDoer SubDoer) string {
	...
	subDoer(something)
	return somethingElse
}

DoHandler()函数为我们提供了一个包装器,用于使用实际的SubDo()实现调用Do(),我们可以通过跳过处理函数来使用模拟的SubDo()Do()进行单元测试:

// app_test.go
type MockDoer struct {
	counter int
}

func (m *MockDoer) SubDo(string) string {
	m.counter++
	return "something"
}

// 调用来自Do()的模拟函数
func TestDoWithMockedSubDo(t *testing.T) {
	mock := &MockDoer{}
	...
	assert.Equal(t, "something", Do(somethings, mock.SubDo))
	assert.Equal(t, mock.counter, xAmount)
}

// 调用DoHandler(),它使用未模拟的SubDo()调用Do()
func TestDo(t *testing.T) {
	...
	assert.Equal(t, "something else", DoHandler(somethings))
}

// SubDo()不受模拟的影响
func TestSubDo(t *testing.T) {
	...
	assert.Equal(t, "a thing", SubDo(something))
}

英文:

In case someone ends up here on a similar dive down the rabbit hole: I wanted to mock a function called by another function to ensure it's called x times under specific conditions, such as you would do with Jest for example. I wanted to keep the program code as test-agnostic as possible, and having had little success with receiver methods and overriding functions in interfaces I opted for simply passing a function reference as an argument. So if you aren't dead set on an OOP style approach:

// app.go
type SubDoer func(string) string

func SubDo(something string) string {
	...
	return something
}

func DoHandler(somethings []Something) string {
	return Do(somethings, SubDo)
}

func Do(somethings []Something, subDoer SubDoer) string {
	...
	subDoer(something)
	return somethingElse
}

The DoHandler() function gives us a wrapper to call Do() with the actual SubDo() implementation, and we can unit test Do() with a mocked SubDo() by skipping the handler function:

// app_test.go
type MockDoer struct {
	counter int
}

func (m *MockDoer) SubDo(string) string {
	m.counter++
	return "something"
}

// Calls the mocked function from Do()
func TestDoWithMockedSubDo(t *testing.T) {
	mock := &MockDoer{}
	...
	assert.Equal(t, "something", Do(somethings, mock.SubDo))
	assert.Equal(t, mock.counter, xAmount)
}

// Calls DoHandler() which calls Do() with the unmocked SubDo()
func TestDo(t *testing.T) {
	...
	assert.Equal(t, "something else", DoHandler(somethings))
}

// SubDo() is unaffected by the mocks
func TestSubDo(t *testing.T) {
	...
	assert.Equal(t, "a thing", SubDo(something))
}

huangapple
  • 本文由 发表于 2017年3月17日 00:19:48
  • 转载请务必保留本文链接:https://go.coder-hub.com/42839517.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定