如何在Golang中正确地模拟带有成员函数的结构体?

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

How to properly mock a struct with member functions in Golang?

问题

我有两个结构体:FunctionalityClientTestClient,它们都实现了 Interface 接口。我有一个全局变量 Client,类型为 Interface。根据是测试还是正常运行,我将 Client 分配给实际的客户端或模拟客户端。

Interface 接口有一个名为 Request 的方法,我想在测试中模拟这个方法。也就是说,我想要:

  • 记录传递给函数的参数
  • 从函数返回一些任意定义的返回值

所以结构体看起来像这样:

type TestClient struct {
     recordedArgs []interface{}
     returnValues []interface{}
}

func (c *TestClient) Request(body io.Reader, method string, endpoint string, headers []Header) ([]byte, error) {
    c.recordedArgs = append(c.recordedArgs, []interface{}{body, method, endpoint, headers})  // 如果我想要代码可重用,这里不能指定类型
    if len(c.returnValues) != 0 {
        last := c.returnValues[0]
        c.returnValues = c.returnValues[1:]
        return last.([]byte), nil
    }
    return nil, nil
}

我这样使用它:

testClient := TestClient{
    returnValues: []interface{}{
        []byte("任意定义的返回值"),
        []byte("调用 Request 第二次后将返回这个值"),
    }
}
Client = &testClient
// 运行测试
// 现在我们来检查结果
r1 := testClient.recordedArgs[1].([]interface{})  // 因为我将未指定类型的列表附加到 recordedArgs 中
assert.Equal(t, "POST", r1[1].(string))
assert.Equal(t, "/file", r1[2].(string))
// 等等

现在是问题。

我有几个结构体想要像这样进行模拟。目前我只是将上面的代码复制粘贴到每个结构体中。但这真的很糟糕,我希望模拟逻辑能以某种方式抽象出来。我也可以接受类似 Mockito 的 when:当使用特定参数调用模拟函数时,返回特定值并记录调用。

如何正确地在 Golang 中模拟带有成员函数的结构体?

英文:

I have two structs: FunctionalityClient and TestClient, both implementing Interface. I have a global variable Client of type Interface. I assign to Client either the actual client, or the mock client, depending on whether it's a test or a normal run.

Interface has a method Request that I want to mock in tests. That is, I want to:

  • record what were the argument passed to the function
  • return some arbitrarily defined return value from the function

So the struct looks like this:

type TestClient struct {
     recordedArgs []interface{}
     returnValues []interface{}
}
func (c *TestClient) Request(body io.Reader, method string, endpoint string, headers []Header) ([]byte, error) {
	c.recordedArgs = append(c.recordedArgs, []interface{}{body, method, endpoint, headers})  // this can't be typed if I want the code to be reusable
		if len(c.returnValues) != 0 {
		last := c.returnValues[0]
		c.returnValues = c.returnValues[1:]
		return last.([]byte), nil
	}
	return nil, nil
}

And I use it like so:

testClient := TestClient{
	returnValues: []interface{}{
		[]byte("arbitrarily defined return value"),
		[]byte("this will be returned after calling Request a second time"),
	}
}
Client = &testClient
// run the test
// now let's check the results
r1 := testClient.recordedArgs[1].([]interface{})  // because I append untyped lists to recordedArgs
assert.Equal(t, "POST", r1[1].(string))
assert.Equal(t, "/file", r1[2].(string))
// and so on

Now the question.

I have a few structs that I want to mock like this. Currently I just copy and paste the code above for each struct. But that really sucks, I would like the mock logic to be abstracted away somehow. I would also accept something like Mockito's when: when the mocked function is called with specific arguments, return a specific value and record the call.

How can I properly mock a struct with member functions in Golang?

答案1

得分: 0

如果你要模拟HTTP API的客户端,你可以直接使用httptest.Server,这样会大大简化代码。不需要模拟客户端,而是模拟客户端连接的服务器。这个方法非常简单易用,你仍然可以记录请求的方法、路径、主体等信息,并且可以像模拟客户端一样返回任意的响应值。

如果这不是一个选项,你可以将模拟方法抽象出来,以便重复使用:

type TestClient struct {
     recordedArgs [][]interface{}
     returnValues []interface{}
}

func (c *TestClient) mock(args ...interface{}) interface{} {
    c.recordedArgs = append(c.recordedArgs, args)
    if len(c.returnValues) != 0 {
        last := c.returnValues[0]
        c.returnValues = c.returnValues[1:]
        return last
    }
    return nil
}

func (c *TestClient) Request(body io.Reader, method string, endpoint string, headers []Header) ([]byte, error) {
    return c.mock(body, method, endpoint, headers).([]byte), nil
}

这样可以将你的特定用法方法简化为一行代码。

英文:

If you're mocking out clients for HTTP APIs, you might want to just use httptest.Server, which would simplify this tremendously. Rather than mocking out the client, mock out the server the client connects to. It's really easy to use, and you can still record the request method, path, body, etc., as well as returning arbitrary response values the same way you're doing with the mock client.

If that's not an option, you can abstract out your mock method to make it reusable:

type TestClient struct {
     recordedArgs [][]interface{}
     returnValues []interface{}
}

func (c *TestClient) mock(args ...interface{}) interface{} {
	c.recordedArgs = append(c.recordedArgs, args)
	if len(c.returnValues) != 0 {
        last := c.returnValues[0]
        c.returnValues = c.returnValues[1:]
        return last
    }
    return nil
}

func (c *TestClient) Request(body io.Reader, method string, endpoint string, headers []Header) ([]byte, error) {
	return c.mock(body,method,endpoint,headers).([]byte), nil
}

This cuts your usage-specific method down to one line.

huangapple
  • 本文由 发表于 2017年6月14日 21:22:40
  • 转载请务必保留本文链接:https://go.coder-hub.com/44546106.html
匿名

发表评论

匿名网友

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

确定