【翻译】在 Go 中有效编写单元测试的 5 种半技巧

tohearts · · 1448 次点击 · · 开始浏览    
这是一个创建于 的文章,其中的信息可能已经有所发展或是发生改变。

原文地址:[https://betterprogramming.pub/5-and-a-half-techniques-for-effectively-writing-unit-tests-in-go-1b87b94abd21](https://betterprogramming.pub/5-and-a-half-techniques-for-effectively-writing-unit-tests-in-go-1b87b94abd21) 单元测试一直是我的爱好——有点像一种爱好。曾经有一段时间我对它很着迷。 我所有的项目都必须至少有 90% 的单元测试覆盖率。您可能会猜到对代码库进行重大更改需要多长时间。但是,另一方面,很少有人会收到包含与业务逻辑相关的错误的报告。 大多数情况下,我只有与其他(微)服务或数据库的集成问题相关的错误。此外,添加新的业务不变性很容易。已经有测试覆盖了之前的所有案例。 重要的是要确保这些测试最终是绿色的。我经常甚至不检查完整的运行(微)服务——有绿色的新旧单元测试就足够了。 有一次,在处理一个私人项目时,我不得不编写单元测试来覆盖几个模块。里面可能有 100 多个不同的 Go 结构,谁知道有多少功能。我花了整个周末。 星期天晚上很晚,在我睡觉之前,我定了一个闹钟叫醒我,因为我第二天要出差。我几乎没有睡觉。当你做梦时,这是一种奇怪的睡眠方式,但你会以某种方式意识到自己和你的环境。 整个晚上,你的大脑整晚都在活跃。我一直梦想着为我的闹钟编写单元测试。而且,猜猜每个单元测试执行时发生了什么?闹钟一直在响。而且,就这样,整整一夜。哦,是的,我差点忘了说。两年来,我们在生产中实现了零错误。该应用程序仍会获取所有数据。它每周一发送所有电子邮件。我甚至不知道我的 Gitlab 密码。 ## 我们将涵盖的内容 为了节省您阅读整篇文章的时间,您可以直接查看这些主题: - 单元测试和模拟(一般) - 生成模拟接口的部分 - 模拟函数 - 模拟套件和断言 - 模拟 HTTP 服务器 - 模拟 SQL 数据库 ## 单元测试和模拟(一般) 正如我们在 Martin Fowler 的[文章](https://martinfowler.com/bliki/UnitTest.html)中看到的,我们可以区分两种类型的单元测试: - Sociable unit tests测试是我们测试依赖于其他对象的单元的测试。如果您想测试 UserController,您将使用与数据库通信的 UserRepository 对其进行测试。 - Solitary unit tests 是我们测试完全隔离的单元的测试。在这里,您将测试 UserController,它与受控的、模拟的 UserRepository 交互,您可以为它提供它在没有数据库的情况下的准确行为。 这两种方法在一个项目中都是合法的,我总是使用它们。 如果我编写社交单元测试,过程很简单——使用我已经从我的模块中使用的任何东西,并一起测试逻辑。但是,当谈到在 Go 中进行模拟时,它并不是一个标准过程。 Go 不支持继承,但支持组合。这意味着一个结构不扩展第二个结构但包含它。因此,Go 不支持结构级别的多态性,而是支持[接口](https://golangbot.com/polymorphism/)。 因此,一旦您的结构直接依赖于一个结构的实例,或者某个函数需要一个特定的结构作为参数——祝您模拟该结构好运。 在下面的代码示例中,我们有一个使用 UserDBRepository 和 AdminController 的简单案例。AdminController 直接依赖于 UserDBRepository 的实例,它表示负责与数据库“对话”的 Repository 的实现。 ```go type UserDBRepository struct { connection *sql.DB } func (r *UserDBRepository) FilterByLastname(ctx context.Context, lastname string) ([]User, error) { var users []User // // do something with users // return users, nil } type AdminController struct { repository *UserDBRepository } func (c *AdminController) FilterByLastname(ctx *gin.Context) { lastname := ctx.Param("name") c.repository.FilterByLastname(ctx, lastname) // // do something with users // } ``` 如果我们想为 AdminController 编写单元测试,看看它是否会创建正确的 JSON 响应,我们有两种可能性: - 提供 UserDBRepository 的新实例以及到 AdminController 的数据库连接,并希望它是您需要随着时间的推移传递的唯一依赖项。 - 不要提供任何东西,一旦开始运行测试就只期望一个 nil 指针异常。 为了避免这种情况并能够正确测试我们的单元,我们需要确保我们的代码遵守以下原则: 1. [Programming to the interface](https://medium.com/javarevisited/oop-good-practices-coding-to-the-interface-baea84fd60d3)(面向接口编程OOP) 2. [Dependency inversion principle](https://stackify.com/dependency-inversion-principle/)(依赖倒置原则) 一旦我们应用这两个原则,我们重构的代码就会得到如下例所示的形状,其中实际的 AdminController 取决于接口 UserRepository,而不是指定它是数据库的 Repository 还是其他。 ```go type UserRepository interface { GetByID(ctx context.Context, ID string) (*User, error) GetByEmail(ctx context.Context, email string) (*User, error) FilterByLastname(ctx context.Context, lastname string) ([]User, error) Create(ctx context.Context, user User) (*User, error) Update(ctx context.Context, user User) (*User, error) Delete(ctx context.Context, user User) (*User, error) } type AdminController struct { repository UserRepository } func (c *AdminController) FilterByLastname(ctx *gin.Context) { lastname := ctx.Param("name") c.repository.FilterByLastname(ctx, lastname) // // do something with users // } ``` 所以,现在我们有了一个起点,让我们看看如何最有效地模拟。 ## 生成mock 有许多用于生成模拟的库,而且,如果您愿意的话,您可以创建自己的生成器。我喜欢 Vektra 的 Mockery 包。它提供由 Stretchr, Inc 的 Testify 包支持的模拟,这已经是一个足够好的理由继续使用它。 ```go type User struct { // // some fields // } type UserRepository interface { GetByID(ctx context.Context, ID string) (*User, error) GetByEmail(ctx context.Context, email string) (*User, error) FilterByLastname(ctx context.Context, lastname string) ([]User, error) Create(ctx context.Context, user User) (*User, error) Update(ctx context.Context, user User) (*User, error) Delete(ctx context.Context, user User) (*User, error) } type AdminController struct { repository UserRepository } func NewAdminController(repository UserRepository) *AdminController { return &AdminController{ repository: repository, } } func (c *AdminController) FilterByLastname(ctx *gin.Context) { lastname := ctx.Param("name") c.repository.FilterByLastname(ctx, lastname) // // do something with users // } ``` 让我们回到前面的 UserRepository 和 AdminController 示例。AdminController 期望 UserRepository 接口在有人向 /users 端点发送请求时按姓氏过滤用户。 严格来说,**AdminController 并不关心 UserRepository 是如何找到结果的**。根据它是否获得用户或错误的切片,只需要将正确的响应附加到 Gin 包中的上下文。 ```go func main() { var repository UserRepository // // initialize repository // controller := NewAdminController(repository) router := gin.Default() router.GET("/users/:lastname", controller.FilterByLastname) // // do something with router // } ``` 在这个例子中,我使用了 Gin-Gonic 的 Gin 包来进行路由,但是我们想使用哪个包来实现这个目的并不重要。我们将首先初始化 UserRepository 的实际实现,将其传递给 AdminController 并在运行我们的服务器之前定义端点。此时,我们的文件夹结构可能是这样的: ```go user-service | cmd | main.go | pkg | user | user.go | admin_controller.go | admin_controller_test.go ``` 现在,在用户文件夹中,我们可以执行用于生成模拟对象的 Mockery 命令。 `$ mockery --all --case=underscore` 它检查包内的所有接口(您可以进一步调整此选项),并创建一个新文件夹 mocks,其中放置所有生成的文件。 ```go user-service | cmd | main.go | pkg | user | mocks | user_repository.go | user.go | admin_controller.go | admin_controller_test.go ``` 生成文件的内容如下例所示: ```go // Code generated by mockery v1.0.0. DO NOT EDIT. package mocks import ( // // some imports // mock "github.com/stretchr/testify/mock" ) // UserRepository is an autogenerated mock type for the UserRepository type type UserRepository struct { mock.Mock } // Create provides a mock function with given fields: ctx, _a1 func (_m *UserRepository) Create(ctx context.Context, _a1 user.User) (*user.User, error) { ret := _m.Called(ctx, _a1) var r0 *user.User if rf, ok := ret.Get(0).(func(context.Context, user.User) *user.User); ok { r0 = rf(ctx, _a1) } else { if ret.Get(0) != nil { r0 = ret.Get(0).(*user.User) } } var r1 error if rf, ok := ret.Get(1).(func(context.Context, user.User) error); ok { r1 = rf(ctx, _a1) } else { r1 = ret.Error(1) } return r0, r1 } // and so on.... ``` 当我在一个项目上工作时,我喜欢将所有命令都写在项目内部的某个地方。有时,它可以是 Makefile 或 bash 脚本。但是在这里,我们可以在用户文件夹中添加额外的 generate.go 文件,并将以下代码放入其中: ```go package user //go:generate go run github.com/vektra/mockery/cmd/mockery -all -case=underscore ``` 目录结构: ```go user-service | cmd | main.go | pkg | user | mocks | user_repository.go | user.go | admin_controller.go | admin_controller_test.go | generate.go ``` 该文件包含一个特定的注释,以 //go:generate 开头。它包括一个用于执行其后代码的标志,一旦您从项目根文件夹中的波纹管运行命令,它将生成所有文件: ```go $ go generate ./... ``` 这两种方法最终都会给出相同的结果——生成带有模拟对象的文件。因此,编写单独的单元测试不再是问题: ```go func TestAdminController(t *testing.T) { var ctx *gin.Context // // setup context // repository := &mocks.UserRepository{} repository. On("FilterByLastname", ctx, "some last name"). Return(nil, errors.New("some error")). Once() controller := NewAdminController(repository) controller.FilterByLastname(ctx) // // do some checking for ctx // } ``` ## 接口的部分模拟 有时,不需要模拟接口中的所有方法。或者,包不属于我们所有,因此我们无法生成文件。在我们的库中创建和保存文件也没有意义。 但是,有时候,接口可以有很多方法,我们需要其中的一些方法。对于这种情况,我们仍然可以使用 UserRepository 的示例。AdminController 只使用 Repository 中的一个函数,称为 FilterByLastname。 这意味着我们不需要任何其他方法来测试 AdminController。为此,让我们提供一些名为 MockedUserRepository 的结构,在下面的示例中可见: ```go type MockedUserRepository struct { UserRepository filterByLastnameFunc func(ctx context.Context, lastname string) ([]User, error) } func (r *MockedUserRepository) FilterByLastname(ctx context.Context, lastname string) ([]User, error) { return r.filterByLastnameFunc(ctx, lastname) } ``` MockedUserRepository 实现接口 UserRepository。我们确保在将 UserRepository 接口嵌入 MockedUserRepository 时就是这种情况。 我们的模拟对象期望在其中包含一些 UserRepository 接口的实例。如果未定义该实例,默认情况下将为 nill 。除此之外,它还有一个字段,这是一种函数。 此函数与 FilterByLastname 具有相同的签名。FilterByLastname 方法附加到模拟结构,它只是代理对私有字段的调用。现在,如果我们按以下方式重写我们的测试,它可能看起来更直观: ```go func TestAdminController(t *testing.T) { var gCtx *gin.Context // // setup context // repository := &MockedUserRepository{} repository.filterByLastnameFunc = func(ctx context.Context, lastname string) ([]User, error) { if ctx != gCtx { t.Error("expected other context") } if lastname != "some last name" { t.Error("expected other lastname") } return nil, errors.New("error") } controller := NewAdminController(repository) controller.FilterByLastname(gCtx) // // do some checking for ctx // } ``` 当我们使用 AWS SDK 测试我们的代码与 AWS 服务(如 SQS)的集成时,这种技术可能会很有用。在这种情况下,我们的 SQSReceiver 依赖于 SQSAPI 接口,它有……。嗯,很多功能: ```go import ( // // some imports // "github.com/aws/aws-sdk-go/service/sqs/sqsiface" ) type SQSReceiver struct { sqsAPI sqsiface.SQSAPI } func (r *SQSReceiver) Run() { // // wait for SQS message // } ``` 在这里我们可以使用相同的技术并提供我们自己的模拟结构: ```go type MockedSQSAPI struct { sqsiface.SQSAPI sendMessageFunc func(input *sqs.SendMessageInput) (*sqs.SendMessageOutput, error) } func (m *MockedSQSAPI) SendMessage(input *sqs.SendMessageInput) (*sqs.SendMessageOutput, error) { return m.sendMessageFunc(input) } func TestSQSReceiver(t *testing.T) { // // setup context // sqsAPI := &MockedSQSAPI{} sqsAPI.sendMessageFunc = func(input *sqs.SendMessageInput) (*sqs.SendMessageOutput, error) { if input.MessageBody == nil || *input.MessageBody != "content" { t.Error("expected other message") } return nil, errors.New("error") } receiver := &SQSReceiver{ sqsAPI: sqsAPI, } receiver.Run() // // do some checking for ctx // } ``` 通常,**我不会测试负责与数据库或外部服务建立连接的基础设施对象**。为此,我在更高级别的测试金字塔上编写测试。尽管如此,如果确实需要测试此类代码,这种方法对我还是有帮助的。 ## 函数模拟 在核心 Go 代码或任何其他包中,有许多有用的函数。我们可以直接在代码中使用这些函数,例如下面的 ConfigurationRepository 中。 该结构体负责读取 config.yml 文件并返回应用程序各处使用的配置。ConfigurationRepository 从核心 Go 包 IOutil 中调用 ReadFile 方法: ```go type ConfigurationRepository struct { // // some fields // } func (r *ConfigurationRepository) GetConfiguration() (map[string]string, error) { config := map[string]string{} data, err := ioutil.ReadFile("config.yml") // // do something with data // return config, nil } ``` 在这样的代码中,如果我们要测试GetConfiguration,那么每次测试执行都不可避免地要依赖config.yml文件的存在。 我们再次依赖技术细节,比如从文件中读取。当这样的事情发生时,我想为这段代码提供单元测试,我过去使用了两种变体。 ### 变体 1:简单类型别名 第一个变体是为我们要模拟的方法类型提供类型别名。新类型表示我们要在代码中使用的函数签名。ConfigurationRepository 应该依赖于这个新类型 FileReaderFunc 而不是我们想要模拟的方法: ```go type FileReaderFunc func(filename string) ([]byte, error) type ConfigurationRepository struct { fileReaderFunc FileReaderFunc // // some fields // } func NewConfigurationRepository(fileReaderFunc FileReaderFunc) ConfigurationRepository{ return ConfigurationRepository{ fileReaderFunc: fileReaderFunc, } } func (r *ConfigurationRepository) GetConfiguration() (map[string]string, error) { config := map[string]string{} data, err := r.fileReaderFunc("config.yml") // // do something with data // return config, nil } ``` 在这种情况下,在初始化您的应用程序时,我们将在创建配置存储库期间将 Go 核心包中的实际方法作为参数传递: ```go package main func main() { repository := NewConfigurationRepository(ioutil.ReadFile) config, err := repository.GetConfiguration() // // do something with configuration // } ``` 最后,我们可以像下面的代码示例一样编写单元测试。我们在那里定义了一个新的读取器函数,它返回我们在每种情况下控制的结果。 ```go func TestGetConfiguration(t *testing.T) { var readerFunc FileReaderFunc // we want to have error from reader readerFunc = func(filename string) ([]byte, error) { return nil, errors.New("error") } repository := NewConfigurationRepository(readerFunc) _, err := repository.GetConfiguration() if err == nil { t.Error("error is expected") } // we want to have concrete result from reader readerFunc = func(filename string) ([]byte, error) { return []byte("content"), nil } repository = NewConfigurationRepository(readerFunc) _, err = repository.GetConfiguration() if err != nil { t.Error("error is not expected") } // // do something with config // } ``` ### 变体 2:使用接口的复杂类型别名 第二种变体使用相同的想法,但将接口作为 ConfigurationRepository 中的依赖项。它不依赖于函数类型,而是依赖于接口 FileReader,该接口的方法与我们要模拟的 ReadFile 方法具有相同的签名。 ```go type FileReader interface { ReadFile(filename string) ([]byte, error) } type ConfigurationRepository struct { fileReader FileReader // // some fields // } func NewConfigurationRepository(fileReader FileReader) *ConfigurationRepository { return &ConfigurationRepository{ fileReader: fileReader, } } func (r *ConfigurationRepository) GetConfiguration() (map[string]string, error) { config := map[string]string{} data, err := r.fileReader.ReadFile("config.yml") // // do something with data // return config, nil } ``` 此时,我们应该再次添加相同的类型别名 FileReaderFunc,但这次我们应该为该类型附加一个函数。是的,我们需要在一个方法中添加一个方法——我无法表达我有多喜欢 Go 中的这一部分。 ```go type FileReaderFunc func(filename string) ([]byte, error) func (f FileReaderFunc) ReadFile(filename string) ([]byte, error) { return f(filename) } ``` 从这一点来看,FileReaderFunc 类型实现了 FileReader 接口。它拥有的唯一方法代理调用该类型的实例,即原始方法。当我们想要初始化应用程序时,它带来了最小的变化: ```go func main() { repository := NewConfigurationRepository(FileReaderFunc(ioutil.ReadFile)) config, err := repository.GetConfiguration() // // do something with configuration // } ``` 而且,它不对单元测试进行任何更改: ```go func TestGetConfiguration(t *testing.T) { var readerFunc FileReaderFunc // we want to have error from reader readerFunc = func(filename string) ([]byte, error) { return nil, errors.New("error") } repository := NewConfigurationRepository(readerFunc) _, err := repository.GetConfiguration() if err == nil { t.Error("error is expected") } // we want to have concrete result from reader readerFunc = func(filename string) ([]byte, error) { return []byte("content"), nil } repository = NewConfigurationRepository(readerFunc) config, err := repository.GetConfiguration() if err != nil { t.Error("error is not expected") } // // do something with config // } ``` 我更喜欢第二种变体,因为比起独立函数,我更喜欢接口和结构。但是,这两种解决方案中的任何一种都是好的。 ## Suites and Assertions 我需要再次提到 Testify 包的伟大之处。除了模拟之外,这个库还提供对套件和断言的支持。 只要测试的重点是检查一个简单的函数或没有任何依赖项(或至少不是模拟的)的结构,我就会对简单的单元测试使用断言。我发现对于未来的代码读者来说,理解测试用例的想法是有帮助的,而且更明确。 我唯一一次使用纯 Go 原生代码进行测试是在**我的库还没有依赖其他包的时候**,所以保持它“干净”。否则,如果没有这种包的帮助,覆盖所有检查太累人了。 ```go import ( // // some imports // "github.com/stretchr/testify/assert" ) func TestUser_HasAccess(t *testing.T) { assert.False(t, User{}.HasAccess("admin")) assert.True(t, User{ Roles; []string{"admin"} }.HasAccess("admin")) } ``` 当应该测试一些复杂的结构时,我使用套件,至少有一个模拟依赖项。这使我可以定义一个代码,该代码应在整个套件开始之前、每次测试之前、每次测试之后等执行。 在大多数情况下,在每个 Suite 运行之前,我都会初始化对整个过程来说都是静态的变量。 例如,如果 Context 或 Request 不包含任何特定于测试用例的数据,我会在 Suite 启动之前定义它们。如果测试可以改变它们的状态,我会在每次测试前用所有模拟对象和主要结构初始化它们。 最后,在每次测试之后,有时我应该放置一个代码来关闭一些通道,销毁一些变量,或者对发送到结构函数的调用次数进行断言。 ```go import ( // // some imports // "github.com/stretchr/testify/suite" ) type AdminControllerTestSuite struct { suite.Suite controller *AdminController repository *mocks.UserRepository ctx *gin.Context } func TestAdminControllerTestSuite(t *testing.T) { suite.Run(t, &AdminControllerTestSuite{}) } func (s *AdminControllerTestSuite) SetupSuite() { s.ctx = &gin.Context() } func (s *AdminControllerTestSuite) SetupTest() { s.repository = &mocks.UserRepository{} s.controller = NewAdminController(s.repository) } func (s *AdminControllerTestSuite) TearDownTest() { s.repository.AssertExpectations(s.T()) } func (s *AdminControllerTestSuite) TestFind_ProfessionError() { s.repository. On("FilterByLastname", s.ctx, "some last name"). Return(nil, errors.New("some error")). Once() s.controller.FilterByLastname(s.ctx) // // do some checking for s.ctx // } ``` ## 模拟 HTTP 服务器 当谈到模拟 HTTP 服务器时,我认为它不属于单元测试。尽管如此,有时某些人的代码结构可能依赖于某些 HTTP 请求,本节给出了这些情况下的一些想法。 让我们假设我们有一些 UserAPIRepository,它通过与外部 API 而不是数据库通信来发送和获取数据。这个结构可能看起来像这样: ```go type UserAPIRepository struct { host string } func NewUserAPIRepository(host string) *UserAPIRepository { return &UserAPIRepository{ host: host, } } func (r *UserAPIRepository) FilterByLastname(ctx context.Context, lastname string) ([]User, error) { var users []User url := path.Join(r.host, "/users/", lastname) response, err := http.Get(url) // // do somethinf with users // return users, nil } ``` 当然,我们也可以用 mocking 函数接近这里,但让我们继续玩游戏。为了对 UserAPIRepository 进行单元测试,我们可以使用核心 Go HTTPtest 包中的 Server 实例。 这个包为我们提供了一个简单的小型服务器,在本地的一些端口上工作,我们可以快速适应我们的测试用例并向它发送请求: ```go import ( // // some imports // "net/http/httptest" ) func TestUserAPIRepository(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if strings.HasPrefix(r.URL.Path, "/users/") { var content string // // do something // io.WriteString(w, content) return } http.NotFound(w, r) })) repository := NewUserAPIRepository(server.URL) users, err := repository.FilterByLastname(context.Background(), "some last name") // // do some checking for users and err // } ``` ## 模拟 SQL 数据库 同样,与 HTTP 请求一样,我并不是特别渴望编写用于测试 SQL 查询的单元测试。我总是质疑自己是在那里测试存储库还是测试模拟工具。 尽管如此,当我想检查一些 SQL 查询时,它可能包含在一些结构中,就像这里的 UserDBRepository: ```go type UserDBRepository struct { connection *sql.DB } func NewUserDBRepository(connection *sql.DB) *UserDBRepository { return &UserDBRepository{ connection: connection, } } func (r *UserDBRepository) FilterByLastname(ctx context.Context, lastname string) ([]User, error) { var users []User rows, err := r.connection.Query("SELECT * FROM users WHERE lastname = ?", lastname) // // do something with users // return users, nil } ``` 当我决定为这种存储库编写单元测试时,我喜欢使用 DATA-DOG 的 [Sqlmock](https://github.com/DATA-DOG/go-sqlmock) 包。它很简单并且有很好的文档: ```go import ( // // some imports // "github.com/DATA-DOG/go-sqlmock" ) func TestUserDBRepository(t *testing.T) { db, mock, err := sqlmock.New() if err != nil { t.Error("expected not to have error") } mock. ExpectQuery("SELECT * FROM users WHERE lastname = ?"). WithArgs("some last name"). WillReturnError(errors.New("error")) repository := NewUserDBRepository(db) users, err := repository.FilterByLastname(context.Background(), "some last name") // // do some checking for users and err // } ``` 当模拟实际的 SQL 查询太累时,另一种方法是使用一个带有测试数据的小 SQLite 文件。它应该与我们的常规 SQL 数据库具有相同的表结构。 当然,这也不是一个理想的解决方案,因为我们在不同的数据库引擎上测试我们的查询,我们可能应该依赖 ORM 来避免双重集成。 在本例中,我创建了一个临时文件,并在每次测试执行之前将数据从 SQLite 文件复制到其中。它比较慢,但像这样,我不能破坏我的测试数据。 ```go import ( // // some imports // _ "github.com/mattn/go-sqlite3" ) func getSqliteDBWithTestData() (*sql.DB, error) { // read all from sqlite file data, err := ioutil.ReadFile("test_data.sqlite") if err != nil { return nil, err } // create temporary file tmpFile, err := ioutil.TempFile("", "db*.sqlite") if err != nil { return nil, err } // store test data into temporary file _, err = tmpFile.Write(data) if err != nil { return nil, err } err = tmpFile.Close() if err != nil { return nil, err } // make connection to temporary file db, err := sql.Open("sqlite3", tmpFile.Name()) if err != nil { return nil, err } return db, nil } ``` 最后,单元测试现在看起来更简单了: ```go func TestUserDBRepository(t *testing.T) { db, err := getSqliteDBWithTestData() if err != nil { t.Error("expected not to have error") } repository := NewUserDBRepository(db) users, err := repository.FilterByLastname(context.Background(), "some last name") // // do some checking for users and err // } ``` ## 结论 用 Go 编写单元测试仍然比用其他语言更具挑战性,至少对我来说是这样。它需要准备代码以支持测试策略。 这在某种程度上是一个令人愉快的部分——它比任何其他语言都更能帮助我塑造我编写代码的架构方法。 它从不乏味,即使经过一千次单元测试,它也给人一种持续的愉悦感。您在 Go 中进行单元测试和模拟的经验如何? ![qrcode_for_gh_283d02e525ed_344.jpg](https://static.golangjob.cn/230802/9f6e5ba31b4aca8ff5cb76b5952cfbca.jpg)

有疑问加站长微信联系(非本文作者))

入群交流(和以上内容无关):加入Go大咖交流群,或添加微信:liuxiaoyan-s 备注:入群;或加QQ群:692541889

1448 次点击  
加入收藏 微博
被以下专栏收入,发现更多相似内容
添加一条新回复 (您需要 登录 后才能回复 没有账号 ?)
  • 请尽量让自己的回复能够对别人有帮助
  • 支持 Markdown 格式, **粗体**、~~删除线~~、`单行代码`
  • 支持 @ 本站用户;支持表情(输入 : 提示),见 Emoji cheat sheet
  • 图片支持拖拽、截图粘贴等方式上传