Efficient practice of Go unit testing

A well-known development method in agile development is XP (Extreme Programming). XP advocates testing first. In order to minimize the chance of bugs in the future, this is similar to TDD (Test Driven Development), which has become popular in recent years. .

When I first started programming, I always ignored the role of unit tests in the code, and felt that the effort to write unit tests caught up with or even surpassed business programs. Later, the business volume became more and more complicated. Slowly, a problem emerged, that is, the system is a black box for testers, and simple tests cannot guarantee that everything designed by the system can be tested⬇️ Give two simplest
examples :
The data management of the system design cannot be tested from the functional business, and for the testers, the use cases may not be covered due to version differences.
If there are two fields in a table, after a new user comes to update one field, he will no longer operate as a new user when testing the function of another field.
In such cases, if the developers do not check the system thoroughly, problems are likely to occur.
Based on the above situation, it is necessary to conduct an "expectation" test on the function from the perspective of the developer. After a function has passed, what should be input, what should be output, what data has changed, whether the change meets expectations, and so on.

Recently, the company's business has basically been transferred to Go for development, and the entire business processing of Go has been gradually improved, and the unit test of Go is also very easy to use, so I will make a small summary.

1. Mock DB

In unit testing, a very important item is the Mock of the database. The database should be used as a clean initial state in each unit test, and the running speed should not be too slow each time.

1. Mock of Mysql

What is used here is github.com/dolthub/go-mysql-server, borrowing from this big brother's method, how to perform Fake test for MySQL

  • DB initialization
    in the db directory
type Config struct {
    
    
   DSN             string // write data source name.
   MaxOpenConn     int    // open pool
   MaxIdleConn     int    // idle pool
   ConnMaxLifeTime int
}

var DB *gorm.DB

// InitDbConfig 初始化Db
func InitDbConfig(c *conf.Data) {
    
    
   log.Info("Initializing Mysql")
   var err error
   dsn := c.Database.Dsn
   maxIdleConns := c.Database.MaxIdleConn
   maxOpenConns := c.Database.MaxOpenConn
   connMaxLifetime := c.Database.ConnMaxLifeTime
   if DB, err = gorm.Open(mysql.Open(dsn), &gorm.Config{
    
    
      QueryFields: true,
      NamingStrategy: schema.NamingStrategy{
    
    
         //TablePrefix:   "",   // 表名前缀
         SingularTable: true, // 使用单数表名
      },
   }); err != nil {
    
    
      panic(fmt.Errorf("初始化数据库失败: %s \n", err))
   }
   sqlDB, err := DB.DB()
   if sqlDB != nil {
    
    
      sqlDB.SetMaxIdleConns(int(maxIdleConns))                               // 空闲连接数
      sqlDB.SetMaxOpenConns(int(maxOpenConns))                               // 最大连接数
      sqlDB.SetConnMaxLifetime(time.Second * time.Duration(connMaxLifetime)) // 单位:秒
   }
   log.Info("Mysql: initialization completed")
}
  • Initialization and injection of fake-mysql
    In the fake_mysql directory
var (
   dbName    = "mydb"
   tableName = "mytable"
   address   = "localhost"
   port      = 3380
)

func InitFakeDb() {
    
    
   go func() {
    
    
      Start()
   }()
   db.InitDbConfig(&conf.Data{
    
    
      Database: &conf.Data_Database{
    
    
         Dsn:             "no_user:@tcp(localhost:3380)/mydb?timeout=2s&readTimeout=5s&writeTimeout=5s&parseTime=true&loc=Local&charset=utf8,utf8mb4",
         ShowLog:         true,
         MaxIdleConn:     10,
         MaxOpenConn:     60,
         ConnMaxLifeTime: 4000,
      },
   })
   migrateTable()
}

func Start() {
    
    
   engine := sqle.NewDefault(
      memory.NewMemoryDBProvider(
         createTestDatabase(),
         information_schema.NewInformationSchemaDatabase(),
      ))

   config := server.Config{
    
    
      Protocol: "tcp",
      Address:  fmt.Sprintf("%s:%d", address, port),
   }

   s, err := server.NewDefaultServer(config, engine)
   if err != nil {
    
    
      panic(err)
   }

   if err = s.Start(); err != nil {
    
    
      panic(err)
   }

}

func createTestDatabase() *memory.Database {
    
    
   db := memory.NewDatabase(dbName)
   db.EnablePrimaryKeyIndexes()
   return db
}

func migrateTable() {
    
    
// 生成一个user表到fake mysql中
   err := db.DB.AutoMigrate(&model.User{
    
    })
   if err != nil {
    
    
      panic(err)
   }
}

At the beginning of the unit test, InitFakeDb()just call

func setup() {
    
    
   fake_mysql.InitFakeDb()
}

2. Mock of Redis

What is used here is miniredis , and the matching Redis Client is go-redis/redis/v8to call InitTestRedis() here to inject

// RedisClient redis 客户端  
var RedisClient *redis.Client  
  
// ErrRedisNotFound not exist in redisconst ErrRedisNotFound = redis.Nil  
  
// Config redis config
type Config struct {
    
      
   Addr         string  
   Password     string  
   DB           int  
   MinIdleConn  int  
   DialTimeout  time.Duration  
   ReadTimeout  time.Duration  
   WriteTimeout time.Duration  
   PoolSize     int  
   PoolTimeout  time.Duration  
   // tracing switch  
   EnableTrace bool  
}  
  
// Init 实例化一个redis client  
func Init(c *conf.Data) *redis.Client {
    
      
   RedisClient = redis.NewClient(&redis.Options{
    
      
      Addr:         c.Redis.Addr,  
      Password:     c.Redis.Password,  
      DB:           int(c.Redis.DB),  
      MinIdleConns: int(c.Redis.MinIdleConn),  
      DialTimeout:  c.Redis.DialTimeout.AsDuration(),  
      ReadTimeout:  c.Redis.ReadTimeout.AsDuration(),  
      WriteTimeout: c.Redis.WriteTimeout.AsDuration(),  
      PoolSize:     int(c.Redis.PoolSize),  
      PoolTimeout:  c.Redis.PoolTimeout.AsDuration(),  
   })  
  
   _, err := RedisClient.Ping(context.Background()).Result()  
   if err != nil {
    
      
      panic(err)  
   }  
  
   // hook tracing (using open telemetry)  
   if c.Redis.IsTrace {
    
      
      RedisClient.AddHook(redisotel.NewTracingHook())  
   }  
  
   return RedisClient  
}  
  
// InitTestRedis 实例化一个可以用于单元测试的redis  
func InitTestRedis() {
    
      
   mr, err := miniredis.Run()  
   if err != nil {
    
      
      panic(err)  
   }  
   // 打开下面命令可以测试链接关闭的情况  
   // defer mr.Close()  
  
   RedisClient = redis.NewClient(&redis.Options{
    
      
      Addr: mr.Addr(),  
   })  
   fmt.Println("mini redis addr:", mr.Addr())  
}

2. Unit testing

After comparison, I chose goconvey , a unit testing framework
, which is much easier to use than native go testing. goconvey also provides many useful functions:

  • Multi-level nested single test
  • rich assertion
  • Clear single test results
  • Support native go test

use

go get github.com/smartystreets/goconvey
func TestLoverUsecase_DailyVisit(t *testing.T) {
    
      
   Convey("Test TestLoverUsecase_DailyVisit", t, func() {
    
      
      // clean  
      uc := NewLoverUsecase(log.DefaultLogger, &UsecaseManager{
    
    })  
  
      Convey("ok", func() {
    
      
         // execute  
         res1, err1 := uc.DailyVisit("user1", 3)  
         So(err1, ShouldBeNil)  
         So(res1, ShouldNotBeNil)  
         // 第 n (>=2)次拜访,不应该有奖励,也不应该报错  
         res2, err2 := uc.DailyVisit("user1", 3)  
         So(err2, ShouldBeNil)  
         So(res2, ShouldBeNil)  
      })  
   })  
}
可以看到,函数签名和 go 原生的 test 是一致的
测试中嵌套了两层 Convey,外层new了内层Convey所需的参数 
内层调用了函数,对返回值进行了断言

The assertion here can also compare the return value like this So(x, ShouldEqual, 2)
or judge the length, etc.So(len(resMap),ShouldEqual, 2)

The nesting of Convey can also be flexible and multi-layered, and can be expanded like a multi-fork tree, which is enough for business simulation.


3. TestMain

Add a TestMain as a unified entry for all cases

import (  
"os"  
"testing"  
  
. "github.com/smartystreets/goconvey/convey"  
)  
  
func TestMain(m *testing.M) {
    
      
   setup()  
   code := m.Run()  
   teardown()  
   os.Exit(code)
}
// 初始化fake db
func setup() {
    
      
   fake_mysql.InitFakeDb()  
   redis.InitTestRedis()
}

Guess you like

Origin blog.csdn.net/w_monster/article/details/130294825