Testing/mocking 3rd party packages in Golang

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

Testing/mocking 3rd party packages in Golang

问题

我是新手学习Golang,并且在学习这门语言时采用了TDD的方法。我一直进展得还不错,但是我发现测试第三方包有些麻烦,这让我觉得我可能采取了错误的方法。

我遇到的具体问题是在模拟Redis客户端进行错误处理时。我采取的方法是创建自己的接口,然后实现包装我想使用的客户端方法。

type Redis interface {
    Get(key string) (string, error)
}

type RedisClient struct {
    client *redis.Client
}

func (redisClient *RedisClient) New(client *redis.Client) *RedisClient {
    redisClient.client = client

    return redisClient
}

func (redisClient *RedisClient) Get(key string) (string, error) {
    return redisClient.client.Get(key).Result()
}

然后,我可以创建一个实现相同接口的模拟对象,以返回我指定的任何值,特别是用于测试错误处理。

但是,我遇到了一个问题,客户端的一个特定方法执行事务(MULTI)返回了该包中的另一个接口。在这种情况下,我该怎么办?自己实现那个接口似乎行不通。

同样地,随着对这个客户端的使用增加,我自己的实现可能会变得越来越复杂,甚至实现整个Redis接口,这似乎违背了将这个任务委托给外部依赖的初衷。

有没有更好的方法来测试这样的第三方包,特别是错误处理方面的问题?

英文:

I'm new to Golang and have been taking a TDD approach while learning the language. I've been getting along okay yet I find testing third party packages quite clunky, which leads me to believe that I've been taking the wrong approach.

The specific case I'm having trouble with is mocking a Redis client for error handling. The approach I've taken is to create my own interface, and the implementation wraps the clients methods that I want to use.

type Redis interface {
	Get(key string) (string, error)
}

type RedisClient struct {
	client *redis.Client
}

func (redisClient *RedisClient) New(client *redis.Client) *RedisClient {
	redisClient.client = client

	return redisClient
}

func (redisClient *RedisClient) Get(key string) (string, error) {
	return redisClient.client.Get(key).Result()
}

I can then create a mock which implements that same interface to return whichever values I specify, particularly for testing error handling.

I've hit a roadblock where a specific method on the client to perform transactions (MULTI) returns another interface belonging to that package. What would I do in this scenario? Implementing that interface myself seems out of the question.

Similarly, as usage of this client grows, my own implementation can grow to the point that it implements the whole interface to Redis - this seems to go against the whole idea of delegating this out to an external dependency.

Is there a better way to test third-party packages like this, for things such as error handling?

答案1

得分: 7

一种方法是创建一个专注于实现目标而不是使用客户端方法的类型。

假设你只想要一个用于保存和获取用户的存储,你可以想象一个如下的接口:

type UserStore interface {
  SaveUser(*User) error
  GetUserByID(id string) (*User, error)
  SearchUsers(query string) ([]User, error)
}

然后你可以实现一个 Redis 版本的存储,并在其中调用任何你想要的客户端方法,这并不重要。你甚至可以实现一个基于 PostgreSQL 的版本或其他任何存储方式。使用这种方法,模拟变得更加容易,因为你只需要实现这个接口而不是 Redis 的接口。

下面是一个模拟接口的示例:

type UserStoreMock struct {
  SaveUserFn      func(*User) error
  SaveUserInvoked bool
  ...
}

func (m *UserStoreMock) SaveUser(u *User) error {
  m.SaveUserInvoked = true

  if m.SaveUserFn != nil {
    return m.SaveUserFn(u)
  }

  return nil
}
...

你可以在测试中使用这个模拟对象,例如:

var m UserStoreMock

m.SaveUserFn = func(u *User) error {
  if u.ID != "123" {
    t.Fail("Bad ID")
  }

  return ErrDuplicateError
}
...

希望这能帮到你!

英文:

One approach would be to create a type that focuses on what you want to accomplish instead on what methods of the client you are using.

Let's say all you want is a storage to save and fetch users, you could imagine an interface like this:

type UserStore interface {
  SaveUser(*User) error
  GetUserByID(id string) (*User, error)
  SearchUsers(query string) ([]User, error)
}

You could then implement a Redis version of this storage and call whatever client methods you want inside, it doesn't matter. You can even implement one in PostgreSQL or whatever.
Also, mocking is way easier with this approach since you all you need to do is to implement this interface instead of the Redis one.

Here is an example of a mock version of this interface:

type UserStoreMock struct {
  SaveUserFn func (*User) error
  SaveUserInvoked bool
  ...
}

func (m *UserStoreMock) SaveUser(u *User) error {
  m.SaveUserInvoked = true

  if m.SaveUserFn != nil {
    return m.SaveUserFn(u)
  }

  return nil
}
...

You can then use this mock in tests like this:

var m UserStoreMock

m.SaveUserFn = func(u *User) error {
  if u.ID != "123" {
    t.Fail("Bad ID")
  }

  return ErrDuplicateError
}
...

huangapple
  • 本文由 发表于 2017年5月2日 02:23:28
  • 转载请务必保留本文链接:https://go.coder-hub.com/43724264.html
匿名

发表评论

匿名网友

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

确定