原文地址:[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)
有疑问加站长微信联系(非本文作者))