Golang 项目开发:有效编写单元测试的技巧 - Mock
项目中的单元测试是一种重要的开发实践。然而,当要测试的代码依赖于其他模块或组件时,编写单元测试就会变得复杂且不稳定。本文演示了如何使用模拟来编写简洁高效的单元测试。
Hallucion
首先,我们看一下项目cmd/server/wire.go
:
提示:google/loija/wire.go
:
TIP线程工具自动编译生成,禁止人为修改存储库、
userHandler
依赖于userService
,而userService
又依赖于`userRepository。
例如handler/user.go代码GetProfile
如下:erService.GetProfile内部调用。 。
因此,在编写单元测试时,我们不可避免地需要初始化userService
实例,而当我们初始化userService时,它依赖于我们❙erArchive
。
明明 我们只需要测试子handler
,但是需要初始化并执行service
service,❀其他代码。这显然违背了单元测试的原则(单一职责原则)。每个单元测试仅关注一个功能点或一个代码单元。
有没有更好的方法来解决这个问题?我们的最终答案是mock
。
Mock(依赖隔离的好帮手)
在单元测试时,我们希望测试被测代码单元的逻辑,并且不想依赖于其他外部模块或组件的状态或行为。这可以更好地隔离被测代码,使测试更加可靠和可重复。
Mock是一种测试模式,模拟或替换被测代码所依赖的外部模块或组件。通过使用Mock对象,我们可以控制外部模块的行为,使得被测代码在测试过程中并不真正依赖和调用外部模块,从而实现被测代码的隔离。
Mock 对象可以模拟外部模块的返回值、异常、超时等,这使得测试更加可管理和可预测。它解决了以下问题:
- 依赖于其他模块:有些代码单元可能依赖于其他模块,比如数据库、Web请求等,通过使用mock对象,我们可以模拟这些依赖关系,使得测试不真正依赖在这些模块上,从而避免测试的不稳定和复杂性。
- 隔离外部环境:有些代码单元会受到外部环境的影响,比如当前时间、系统状态等。Mock对象允许我们控制这些外部环境的状态,从而可以在不同的环境中运行测试环境,从而提高测试覆盖率和准确性。
- 提高测试效率:一些外部模块可以执行网络请求、读写文件等耗时操作,通过使用Mock对象,我们可以避免执行这些操作,从而提高执行速度和效率。 test.中 在NUNU项目中,我们使用以下mock库来编写单元测试
- github.com/Golang/Mock // Google开源mock库
- github.com/Go-Redismock/ V9 // 提供mock用于redis查询的测试,兼容github.com/redis/go-redis/v9
- github.com/DATA-DOG/go-sqlmock // sqlmock是一个模拟库,实现sql/driver命令
需要接口使用
golang/mock
面向导向的编程。我们需要遵循“面向接口的编程”方法来编写我们的
存储库
和服务
。有些同学可能不明白“面向接口编程”是什么意思。我们以代码片段为例:
package repository import ( "github.com/go-nunu/nunu-layout-advanced/internal/model" ) type UserRepository interface { FirstById(id int64) (*model.User, error) } type userRepository struct { *Repository } func NewUserRepository(repository *Repository) *UserRepository { return &UserRepository{ Repository: repository, } } func (r *UserRepository) FirstById(id int64) (*model.User, error) { var user model.User if err := r.db.Where("id = ?", id).First(&user).Error; err != nil { return nil, err } return &user, nil }
在上面的代码中,我们首先定义了
UserRepository接口
,然后通过来实现其方法。
type UserRepository interface { FirstById(id int64) (*model.User, error) } type userRepository struct { *Repository } func (r *UserRepository) FirstById(id int64) (*model.User, error) { // ... }
而不是直接写成
type UserRepository struct { *Repository } func (r *UserRepository) FirstById(id int64) (*model.User, error) { // ... }
的形式,这就是所谓的面向接口的编程。它可以提高代码的灵活性、可扩展性、可测试性和可维护性,深受Go语言推崇。编程风格。
Go-mock 快速入门
golang/mock
真的很容易使用。先安装:go install github.com/golang/mock/mockgen@v1.6.0
mockgen
是一个命令行工具go-mock
,可以自动将代码解析为正确的代码♸
示例:
mockgen -source=internal/service/user.go -destination mocks/service/user.go
上面的命令定义了两个参数,接口源文件和最终生成伪代码的目标文件。我们将目标文件放在mocks/service
目录中。
已经创建了模拟
代码UserService
,我们可以编写单元测试和ler。
最终的单元测试代码如下:
func TestUserHandler_GetProfile(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mockUserService := mock_service.NewMockUserService(ctrl)
// 关键代码,定义mockUserService.GetProfile的返回值
mockUserService.EXPECT().GetProfile(gomock.Any(), userId).Return(&model.User{
Id: 1,
UserId: userId,
Username: "xxxxx",
Nickname: "xxxxx",
Password: "xxxxx",
Email: "xxxxx@gmail.com",
}, nil)
router := setupRouter(mockUserService)
req, _ := http.NewRequest("GET", "/user", nil)
req.Header.Set("Authorization", "Bearer "+token)
resp := httptest.NewRecorder()
router.ServeHTTP(resp, req)
assert.Equal(t, resp.Code, http.StatusOK)
// Add assertions for the response body if needed
}
完整源代码可以在:https://github.com/go-nunu/nunu-layout-advanced/blob/main/test/server/handler /user_test.go
sqlmock 和 redismock
与 handler
和 相比,, 单元测试 repository
稍显不足不同 相同,因为它不再信任我们 它有它的拥有自己的业务模块,但依赖外部数据源,如rpc、redis、MySQL等。
在这种情况下,我们还进行了模拟,以避免连接到真实的数据库和缓存,减少测试的不确定性。
代码如下
package repository
import (
"context"
"testing"
"time"
"github.com/DATA-DOG/go-sqlmock"
"github.com/go-nunu/nunu-layout-advanced/internal/model"
"github.com/go-nunu/nunu-layout-advanced/internal/repository"
"github.com/go-redis/redismock/v9"
"github.com/stretchr/testify/assert"
"gorm.io/driver/mysql"
"gorm.io/gorm"
)
func setupRepository(t *testing.T) (repository.UserRepository, sqlmock.Sqlmock) {
mockDB, mock, err := sqlmock.New()
if err != nil {
t.Fatalf("failed to create sqlmock: %v", err)
}
db, err := gorm.Open(mysql.New(mysql.Config{
Conn: mockDB,
SkipInitializeWithVersion: true,
}), &gorm.Config{})
if err != nil {
t.Fatalf("failed to open gorm connection: %v", err)
}
rdb, _ := redismock.NewClientMock()
repo := repository.NewRepository(db, rdb, nil)
userRepo := repository.NewUserRepository(repo)
return userRepo, mock
}
func TestUserRepository_GetByUsername(t *testing.T) {
userRepo, mock := setupRepository(t)
ctx := context.Background()
username := "test"
// 模拟查询测试数据
rows := sqlmock.NewRows([]string{"id", "user_id", "username", "nickname", "password", "email", "created_at", "updated_at"}).
AddRow(1, "123", "test", "Test", "password", "test@example.com", time.Now(), time.Now())
mock.ExpectQuery("SELECT \\* FROM `users`").WillReturnRows(rows)
user, err := userRepo.GetByUsername(ctx, username)
assert.NoError(t, err)
assert.NotNil(t, user)
assert.Equal(t, "test", user.Username)
assert.NoError(t, mock.ExpectationsWereMet())
}
完整代码可以在:https://github.com/go-nunu/nunu-layout-advanced/blob/main/test/server/repository/user_test.go
测试覆盖率
Golang 官方支持原生生成测试覆盖率报告。
go test -coverpkg=./internal/handler,./internal/service,./internal/repository -coverprofile=./coverage.out ./test/server/...
go tool cover -html=./coverage.out -o coverage.html
上面两个命令创建一个网页视觉覆盖率报告文件coverage.html
。我们可以直接在浏览器中打开。
效果如下: image.png
总结
单元测试是项目中重要的开发实践,可以保证代码的正确性,并提供自动验证功能。在进行单元测试时,我们需要使用面向接口编程,并使用虚拟对象来隔离被测代码的依赖关系。在Go中,我们可以使用golang/mock库来生成mock代码。对于依赖外部数据源的存储库,我们可以使用sqlmock和redismock来模拟数据库和缓存行为。通过使用Mock对象,我们可以控制外部模块的行为,使得被测代码在测试过程中并不真正依赖和调用外部模块,从而实现被测代码的隔离。这提高了测试的可靠性、可重复性和效率。
代码存储库:https://github.com/go-nunu/nunu-layout-advanced/tree/main/test/server
版权声明
本文仅代表作者观点,不代表Code前端网立场。
本文系作者Code前端网发表,如需转载,请注明页面地址。
发表评论:
◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。