Code前端首页关于Code前端联系我们

Golang 项目开发:有效编写单元测试的技巧 - Mock

terry 2年前 (2023-09-24) 阅读数 60 #后端开发

项目中的单元测试是一种重要的开发实践。然而,当要测试的代码依赖于其他模块或组件时,编写单元测试就会变得复杂且不稳定。本文演示了如何使用模拟来编写简洁高效的单元测试。

Hallucion

首先,我们看一下项目cmd/server/wire.go

提示:google/loija/wire.go

TIP线程工具自动编译生成,禁止人为修改存储库、

userHandler依赖于userService,而userService又依赖于`userRepository。

例如handler/user.go代码GetProfile如下:erService.GetProfile内部调用。 。

因此,在编写单元测试时,我们不可避免地需要初始化userService实例,而当我们初始化userService时,它依赖于我们❙erArchive

明明 我们只需要测试子handler,但是需要初始化并执行serviceservice,❀其他代码。这显然违背了单元测试的原则(单一职责原则)。每个单元测试仅关注一个功能点或一个代码单元。

有没有更好的方法来解决这个问题?我们的最终答案是mock

Mock(依赖隔离的好帮手)

在单元测试时,我们希望测试被测代码单元的逻辑,并且不想依赖于其他外部模块或组件的状态或行为。这可以更好地隔离被测代码,使测试更加可靠和可重复。

Mock是一种测试模式,模拟或替换被测代码所依赖的外部模块或组件。通过使用Mock对象,我们可以控制外部模块的行为,使得被测代码在测试过程中并不真正依赖和调用外部模块,从而实现被测代码的隔离。

Mock 对象可以模拟外部模块的返回值、异常、超时等,这使得测试更加可管理和可预测。它解决了以下问题:

  1. 依赖于其他模块:有些代码单元可能依赖于其他模块,比如数据库、Web请求等,通过使用mock对象,我们可以模拟这些依赖关系,使得测试不真正依赖在这些模块上,从而避免测试的不稳定和复杂性。
  2. 隔离外部环境:有些代码单元会受到外部环境的影响,比如当前时间、系统状态等。Mock对象允许我们控制这些外部环境的状态,从而可以在不同的环境中运行测试环境,从而提高测试覆盖率和准确性。
  3. 提高测试效率:一些外部模块可以执行网络请求、读写文件等耗时操作,通过使用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。我们可以直接在浏览器中打开。

效果如下: Golang项目开发:高效编写单元测试的技巧之Mockimage.png

总结

单元测试是项目中重要的开发实践,可以保证代码的正确性,并提供自动验证功能。在进行单元测试时,我们需要使用面向接口编程,并使用虚拟对象来隔离被测代码的依赖关系。在Go中,我们可以使用golang/mock库来生成mock代码。对于依赖外部数据源的存储库,我们可以使用sqlmock和redismock来模拟数据库和缓存行为。通过使用Mock对象,我们可以控制外部模块的行为,使得被测代码在测试过程中并不真正依赖和调用外部模块,从而实现被测代码的隔离。这提高了测试的可靠性、可重复性和效率。

代码存储库:https://github.com/go-nunu/nunu-layout-advanced/tree/main/test/server

版权声明

本文仅代表作者观点,不代表Code前端网立场。
本文系作者Code前端网发表,如需转载,请注明页面地址。

发表评论:

◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。

热门