为单个接口编写测试,并对多个实现进行运行。

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

Writing tests for a single interface and running them for multiple implementations

问题

假设我有以下接口:

type UserRepository interface {
   StoreUser(user User) (User, error)
}

以及以下接口的实现:

type InMemoryRepository struct {
...
}

func (repo InMemoryRepository) StoreUser(user User) (User, error) {
   ...
}

type PostgresqlRepository struct {
...
}

func (repo PostgresqlRepository) StoreUser(user User) (User, error) {
   ...
}

我想为StoreUser方法编写单元测试。但由于所有接口的要求都相同,我只需要编写一次测试。

有什么最佳设计方法可以为接口编写测试,然后使用任何实现运行它?

到目前为止,我想到了两种方法:

a) 在一个测试用例循环中包装测试,包括所有的实现

 testCases := []struct {
    name string
    repo UserRepository
 }{
    { name: "Memory", repo: NewInMemoryRepository()},
    { name: "Postgresql", repo: NewPosrgresqlRepository()},
 }

 for _, tc := range testCases {
 	t.Run(tc.name, func(t *testing) {
 	 ...
 	}
 }

b) 创建一个测试类

 type UserRepositoryTests struct {
 	UserRepository
 }

 func NewUserRepositoryTests(userRepository UserRepository) UserRepositoryTests {
 	return UserRepositoryTests{
 		UserRepository: userRepository,
 	}
 }

 func (userRepository UserRepositoryTests) TestStoreUser(t *testing.T) {
 	...
 }

并从实现包中调用它

 type InMemoryUserRepository struct{}

 func (ss InMemoryUserRepository) StoreUser(user users.User) (users.User, error)

 func TestRunInterfaceTests(t *testing.T) {
 	dsfnjkf := InMemoryUserRepository{}
 	testing := users.NewUserRepositoryTests(dsfnjkf)
 	testing.TestStoreUser(t)
 }

虽然这两种方法都不太合适。

有更好的方法吗?

英文:

Suppose I have the following interface:

type UserRepository interface {
   StoreUser(user User) (User, error)
}

And the following implementations of that interface:

type InMemoryRepository struct {
...
}

func (repo InMemoryRepository) StoreUser(user User) (User, error) {
   ...
}

type PostgresqlRepository struct {
...
}

func (repo PostgresqlRepository) StoreUser(user User) (User, error) {
   ...
}

I want to write a unit test for the StoreUser method. But I only need to write it once since the requirements are the same for all the implementation of that interface.

What's the best design to write the test for the interface and then run it with whatever implementation I have?

So far I came up with either:

a) Wrap the test in a test-case loop with all the implementations

 testCases := []struct {
    name string
    repo UserRepository
 }{
    { name: "Memory", repo: NewInMemoryRepository()},
    { name: "Postgresql", repo: NewPosrgresqlRepository()},
 }


 for _, tc := range testCases {
 	t.Run(tc.name, func(t *testing) {
 	 ...
 	}
 }

b) Create a test class

 type UserRepositoryTests struct {
 	UserRepository
 }

 func NewUserRepositoryTests(userRepository UserRepository) UserRepositoryTests {
 	return UserRepositoryTests{
 		UserRepository: userRepository,
 	}
 }

 func (userRepository UserRepositoryTests) TestStoreUser(t *testing.T) {
 	...
 }

and call it from the implementation package

 type InMemoryUserRepository struct{}

 func (ss InMemoryUserRepository) StoreUser(user users.User) (users.User, error)

 func TestRunInterfaceTests(t *testing.T) {

 	dsfnjkf := InMemoryUserRepository{}

 	testing := users.NewUserRepositoryTests(dsfnjkf)

 	testing.TestStoreUser(t)

 }

Although both ways don't sound right to me.

Is there a better approach?

答案1

得分: 2

第一种方法是在Go中常见的做法,你可以创建一个表格,并使用t.Run迭代运行每个项目。在这种情况下,这是我会使用的方法,实际上我已经多次使用这种方法来实现相同接口的多个实现。

第二种方法在Go中很少见;在测试中嵌套多层通常不符合惯用法 - 测试应该简单明了。

关于你的评论;我认为你描述的可能是接口的编写者希望提供一个测试套件,供其他人在其接口实现上调用。这听起来没问题,但为什么需要那么多层次的抽象?一个名为RunUserRepositoryTests的函数应该足够 - 它将接受你的实现和一个*testing.T

英文:

The first approach, where you create a table and iterate over it running each item with t.Run is idiomatic in Go. It's what I'd use in this scenario, and in fact I've used this approach many times for exactly the same purpose (multiple implementations of the same interface).

The second approach you describe is rare in Go; it's typically not idiomatic to wrap testing in many layers - tests should be simple and explicit.


Regarding your comment; what I think you may be describing is a situation where the writer of the interface wants to provide a test suite that others can invoke on their implementations of the interface. That sounds OK, but why do you need all those levels of abstraction? A function like RunUserRepositoryTests should be sufficient - it will take your implementation and a *testing.T.

huangapple
  • 本文由 发表于 2021年11月12日 05:15:14
  • 转载请务必保留本文链接:https://go.coder-hub.com/69935006.html
匿名

发表评论

匿名网友

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

确定