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

Viewed 104

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 Answers

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.

Related