前言
明天咱们先来看看无关数据层(repo)的单元测试应该如何实际。
数据层,就是咱们经常说的 repo/dao
,其性能就是和数据库、缓存或者其余数据源打交道。它须要从数据源中获取数据,并返回给上一层。在这一层通常没有简单业务的逻辑,所以最重要的就是测试各个数据字段的编写是否正确,以及 SQL 等查问条件是否失常能被筛选。
当然,数据层也基本上是最底层了,通常这一层的单元测试更加的重要,因为如果一个字段名称和数据库不统一下层所有依赖这个办法的中央全副都会报错。
因为数据层和数据源打交道,那么测试的麻烦点就在于,通常咱们不能要求外接肯定能提供一个数据源供咱们测试:一方面是因为咱们不可能随时都能连上测试服务器的数据库,另一方面咱们也不能要求单元测试运行的时候只有你一个人在应用这个数据库,而且数据库数据洁净。退一步讲,咱们也没方法 mock,如果 mock 了 sql,那么测试的意义就不大了。
上面咱们就以咱们常见的 mysql 数据库为例,看看在 golang 中如何进行单元测试的编写。
筹备工作的阐明
数据源
首先,咱们须要一个洁净的数据源,因为咱们没有方法依赖于内部服务器的数据库,那么咱们就利用最常应用的 docker
来帮忙咱们构建一个所须要应用的数据源。
咱们这里应用 github.com/ory/dockertest 来帮忙咱们构建测试的环境,它能帮忙咱们启动一个所须要的环境,当然你也能够抉择手动应用 docker 或者 docker-compose 来创立。
初始数据
有了数据库之后,咱们还须要表构造和初始数据,这部分也有两种计划:
- 应用 orm 提供的 sync/migration 相似的性能,将构造体间接映射生成表字段,通过 go 代码创立初始数据
- 间接应用 sql 语句,通过执行 sql 语句来创立对应的表构造和字段数据
本案例应用第一种形式进行,第二种也相似
根本 case 代码
咱们首先来疾速搞定一下默认的 case 代码,也就是咱们经常搬砖的 CRUD。(这里仅给出最根本的实现,重点次要关注在单元测试上)
package repoimport ( "context" "go-demo/m/unit-test/entity" "xorm.io/xorm")type UserRepo interface { AddUser(ctx context.Context, user *entity.User) (err error) DelUser(ctx context.Context, userID int) (err error) GetUser(ctx context.Context, userID int) (user *entity.User, exist bool, err error)}type userRepo struct { db *xorm.Engine}func NewUserRepo(db *xorm.Engine) UserRepo { return &userRepo{db: db}}func (ur userRepo) AddUser(ctx context.Context, user *entity.User) error { _, err := ur.db.Insert(user) return err}func (ur userRepo) DelUser(ctx context.Context, userID int) error { _, err := ur.db.Delete(&entity.User{ID: userID}) return err}func (ur userRepo) GetUser(ctx context.Context, userID int) (user *entity.User, exist bool, err error) { user = &entity.User{ID: userID} exist, err = ur.db.Get(user) return user, exist, err}
初始化测试环境
首先创立 repo_main_test.go
文件
package repoimport ( "database/sql" "fmt" "testing" "time" _ "github.com/go-sql-driver/mysql" "github.com/ory/dockertest/v3" "github.com/ory/dockertest/v3/docker" "go-demo/m/unit-test/entity" "xorm.io/xorm" "xorm.io/xorm/schemas")type TestDBSetting struct { Driver string ImageName string ImageVersion string ENV []string PortID string Connection string}var ( mysqlDBSetting = TestDBSetting{ Driver: string(schemas.MYSQL), ImageName: "mariadb", ImageVersion: "10.4.7", ENV: []string{"MYSQL_ROOT_PASSWORD=root", "MYSQL_DATABASE=linkinstar", "MYSQL_ROOT_HOST=%"}, PortID: "3306/tcp", Connection: "root:root@(localhost:%s)/linkinstar?parseTime=true", } tearDown func() testDataSource *xorm.Engine)func TestMain(t *testing.M) { defer func() { if tearDown != nil { tearDown() } }() if err := initTestDataSource(mysqlDBSetting); err != nil { panic(err) } if ret := t.Run(); ret != 0 { panic(ret) }}func initTestDataSource(dbSetting TestDBSetting) (err error) { connection, imageCleanUp, err := initDatabaseImage(dbSetting) if err != nil { return err } dbSetting.Connection = connection testDataSource, err = initDatabase(dbSetting) if err != nil { return err } tearDown = func() { testDataSource.Close() imageCleanUp() } return nil}func initDatabaseImage(dbSetting TestDBSetting) (connection string, cleanup func(), err error) { pool, err := dockertest.NewPool("") pool.MaxWait = time.Minute * 5 if err != nil { return "", nil, fmt.Errorf("could not connect to docker: %s", err) } resource, err := pool.RunWithOptions(&dockertest.RunOptions{ Repository: dbSetting.ImageName, Tag: dbSetting.ImageVersion, Env: dbSetting.ENV, }, func(config *docker.HostConfig) { config.AutoRemove = true config.RestartPolicy = docker.RestartPolicy{Name: "no"} }) if err != nil { return "", nil, fmt.Errorf("could not pull resource: %s", err) } connection = fmt.Sprintf(dbSetting.Connection, resource.GetPort(dbSetting.PortID)) if err := pool.Retry(func() error { db, err := sql.Open(dbSetting.Driver, connection) if err != nil { fmt.Println(err) return err } return db.Ping() }); err != nil { return "", nil, fmt.Errorf("could not connect to database: %s", err) } return connection, func() { _ = pool.Purge(resource) }, nil}func initDatabase(dbSetting TestDBSetting) (dbEngine *xorm.Engine, err error) { dbEngine, err = xorm.NewEngine(dbSetting.Driver, dbSetting.Connection) if err != nil { return nil, err } err = initDatabaseData(dbEngine) if err != nil { return nil, fmt.Errorf("init database data failed: %s", err) } return dbEngine, nil}func initDatabaseData(dbEngine *xorm.Engine) error { return dbEngine.Sync(new(entity.User))}
上面阐明其中的办法和要点
- TestMain:这个办法是 golang test 的一个个性,它会在所有 单元测试 之前主动执行,特地适宜用于初始化数据和清理测试遗留环境。这个办法中
tearDown
是为了清理连贯和镜像用的 - initDatabaseImage:办法次要就是利用
github.com/ory/dockertest
提供性能拉取一个对应的 docker 镜像并启动 - initDatabaseData:办法次要利用了 xorm 的 Sync 办法去初始化了数据库,当然这里也能够构建你所须要的初始化数据,比方你须要初始化一个超级管理员等等
- testDataSource:咱们将最终初始化的数据源放在了这里,因为后续单元测试的时候应用,这里因为只有一个数据库,就没有封装
编写单元测试
有了后面的筹备工作,单元测试就变得简略了
package repoimport ( "context" "testing" "github.com/stretchr/testify/assert" "go-demo/m/unit-test/entity")func Test_userRepo_AddUser(t *testing.T) { ur := NewUserRepo(testDataSource) user := &entity.User{ Username: "LinkinStar", } err := ur.AddUser(context.TODO(), user) assert.NoError(t, err) dbUser, exist, err := ur.GetUser(context.TODO(), user.ID) assert.NoError(t, err) assert.True(t, exist) assert.Equal(t, user.Username, dbUser.Username) err = ur.DelUser(context.TODO(), user.ID) assert.NoError(t, err)}
能够看到咱们只须要像平时写代码一样间接调用对应的办法就能够进行单元测试了。
- 其中
https://github.com/stretchr/testify
是一个十分好用的断言工具,能帮忙咱们疾速实现单元测试中的断言,以便咱们疾速确定单元测试是否正确。 - 单元测试须要留神的是,咱们这里测试的是增加用户,也就是插入数据,为保障单元测试的独立性,测试完以后办法后数据应该保持一致,故须要进行数据删除,以保障不会烦扰到其余的单元测试。
注意事项
- 本地须要有 docker 环境
- 第一次启动因为须要拉取镜像,依据网络状况不同,拉取工夫不同
- 失常状况下,咱们设定了
AutoRemove
为true
并且不再重启,测试实现之后会将测试应用的 mysql 镜像敞开并删除,然而如果测试意外中断,或者强制中断时,会导致镜像被遗留下来。故,本地测试之后能够应用docker ps
命令查看是否有遗留 - 当然依据所须要的数据源不同,你能够应用其余不同的镜像进行操作,成果是一样的
总结
- repo 数据层的单元测试通过 docker 来启动数据源进行测试
- 应用 orm 或者导入 sql 的形式进行数据初始化
- 测试完单个办法后保障测试前后数据统一,不影响其余单元测试