我应该始终将结构属性定义为接口以便进行可测试性吗?

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

Should I always define structure properties as interface for testability?

问题

据我了解,在Go语言中,模拟结构依赖的唯一方法是使用接口。所以我的问题是:在我的结构体中,如果有一些执行某些操作的方法(结构体不仅仅是存储数据的模型),我是否应该始终将属性定义为接口,以便正确地进行模拟和测试?

简单的例子:

type UserService struct {
    userRepository UserRepository
}

func (us *UserService) MaleUsers() []User {
    all := us.userRepository.FindAll()
    maleUsers := []User{}
    for _, u := range all {
        if u.gender == "male" {
            maleUsers = append(maleUsers, u)
        }
    }
    return maleUsers
}

假设我们有一个用户服务,它有一个依赖:存储库(repository)。
服务有一个方法,该方法获取所有用户,然后按某些条件对它们进行过滤。
顺便说一下,过滤逻辑可以放在一个单独的依赖项中,以避免在服务方法中进行过滤(单一职责原则)。

顺便说一下,我来自Java世界。如果这种应用程序结构的方法在Go中不是惯用的,请告诉我。

英文:

As far as I understand the only way to mock structure dependencies in Go is to use interfaces. So my question is: In cases when my struct has methods which perform some actions (when structure is not just a model storing data), should I always define the properties as interfaces in order to properly mock & test it?

Simple example:

type UserService struct {
    userRepository UserRepository
}

func (us *UserService) MaleUsers() []User {
	all := us.userRepository.FindAll()
	maleUsers := []User{}
	for _, u := range all {
		if u.gender == "male" {
			maleUsers = append(maleUsers, u)
		}
	}
	return maleUsers
}

Imagine we have user service which has a dependency: repository.
Service has method which gets all users and then filters them by some criteria.
By the way filtering logic could go in a separate dependency to avoid filtering in service method (SRP).

Btw. I came from Java world. If this approach of structuring applications is not ideomatic in Go please let me know.

答案1

得分: 1

为了模拟UserService中的userRepository依赖项,你正确地认为最好的方法是使用接口。

首先,创建你的接口:

type UserRepository interface {
    FindAll() []Users
}

然后构建一个模拟对象:

type MockUserRepository struct{}

func (mock MockUserRepository) FindAll() []Users {
    // 在这里手动构建一个用户切片并返回
    return []Users{}
}

最后,在你的测试用例中使用这个模拟对象作为依赖项:

func TestMaleUsers(t *testing.T) {
    // 使用模拟对象组合服务
    service := UserService{
        userRepository: MockUserRepository{},
    }

    // 获取方法调用的输出
    users := service.MaleUsers()

    // 对输出进行断言
}

这样,你就创建了一个接口的模拟对象,可以在测试中使用它,而无需对存储库进行任何数据库调用。

英文:

In order to mock the userRepository dependency of your UserService, you are correct in thinking the best approach would be to use an interface.

Firstly, create your interface:

type UserRepository interface {
	FindAll() []Users
}

Then build a mock:

type MockUserRepository struct{}

func (mock MockUserRepository) FindAll() []Users {
	// here you would manually build a slice of users and return it
	return []Users
}

Finally, use this mock as as a dependency in your test case:

func TestMaleUsers(t *testing.T) {
	// compose service using mock
	service := UserService {
		userRepository: MockUserRepository,
	}
	
	// get output of method call
	users := service.MaleUsers()
	
	// perform assertions on output
}

This way you have created a mock of your interface which can be used in your tests without having to perform any database calls on your repository.

答案2

得分: 0

我猜,如果你想要进行正确的单元测试,使用接口是合理的,就像你在问题中所说的那样。此外,在一些其他语言如Java或C#中,这是一种自然的做法。然而,如果代码没有外部依赖,你也可以轻松地测试你的包的代码而不进行任何更改。

此外,我强烈建议你尽可能多地使用接口,这样你将实现更高的内聚性和更少的耦合性,从而使你的代码更易于维护和测试。

在Go中,一个实体可以隐式地满足一个接口,所以你只需要添加一个接口声明,就可以在将来模拟你的结构。看一些例子:

type MaleIterable interface {
    MaleUsers() []User
}

type UserIterable interface {
    FindAll() []User
}

这是一个代码片段,对你也可能有用。

package main

import "fmt"

type User struct {
    Name   string
    Gender string
    Age    int
}

type UserIterable interface {
    FindAll() []User
}

type FakeUserRepository struct{}

func (ur FakeUserRepository) FindAll() []User {
    return []User{
        User{Name: "Alex", Gender: "male", Age: 19},
        User{Name: "Max", Gender: "male", Age: 34},
        User{Name: "Maria", Gender: "female", Age: 12},
    }
}

type UserService struct {
    userRepository UserIterable
}

func (us *UserService) MaleUsers() []User {
    all := us.userRepository.FindAll()
    maleUsers := []User{}
    for _, u := range all {
        if u.Gender == "male" {
            maleUsers = append(maleUsers, u)
        }
    }
    return maleUsers
}

func main() {
    us := UserService{
        userRepository: FakeUserRepository{},
    }
    males := us.MaleUsers()
    fmt.Println(males)
}
英文:

I guess, it is reasonable to use interfaces if you want to properly perform a unit testing, as you stated in your question. More than that, in some other languages like Java or C#, it is a natural way of doing so. However, you can also easily test the code of your package without any changes if it doesn't have external dependencies.

Besides, I would highly recommend you to use interfaces as much as possible, in that way you would achieve higher cohesion and less coupling, which would make your code more maintainable and testable.

In Go, an entity could satisfy an interface implicitly, so all you need to have an ability to mock your structure in future is to add an interface declaration. Check out some examples:

type MaleIterable interface {
    MaleUsers() []User
}

type UserIterable interface {
    FindAll() []User
}

Here is a code snippet, which could also be useful for you.

package main

import "fmt"

type User struct {
    Name string
    Gender string
    Age int
}

type UserIterable interface {
    FindAll() []User
}

type FakeUserRepository struct {}

func (ur FakeUserRepository) FindAll() []User {
    return []User{
        User{Name:"Alex", Gender:"male", Age:19},
        User{Name:"Max", Gender:"male", Age:34},
        User{Name:"Maria", Gender:"female", Age:12},
    }
}

type UserService struct {
    userRepository UserIterable
}

func (us *UserService) MaleUsers() []User {
    all := us.userRepository.FindAll()
    maleUsers := []User{}
    for _, u := range all {
        if u.Gender == "male" {
            maleUsers = append(maleUsers, u)
        }
    }
    return maleUsers
}

func main() {
    us := UserService{
        userRepository: FakeUserRepository{},
    }
    males := us.MaleUsers()
    fmt.Println(males)
}

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

发表评论

匿名网友

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

确定