Práctica eficiente de pruebas unitarias de Go

Un método de desarrollo muy conocido en el desarrollo ágil es XP (Programación extrema). XP recomienda probar primero. Para minimizar la posibilidad de errores en el futuro, esto es similar a TDD (Desarrollo basado en pruebas), que se ha vuelto popular en los últimos tiempos. años. .

Cuando comencé a programar, siempre ignoré el papel de las pruebas unitarias en el código y sentí que el esfuerzo de escribir pruebas unitarias alcanzó o incluso superó a los programas comerciales. Posteriormente, el volumen de negocio se volvió cada vez más complicado, poco a poco surgió un problema, es decir, el sistema es una caja negra para los probadores, y las pruebas simples no pueden garantizar que todo lo diseñado por el sistema pueda ser probado⬇️ Dé dos ejemplos más simples
:
La gestión de datos del diseño del sistema no se puede probar desde el negocio funcional y, para los evaluadores, es posible que los casos de uso no estén cubiertos debido a diferencias de versión.
Si hay dos campos en una tabla, después de que un nuevo usuario actualice un campo, ya no funcionará como un nuevo usuario al probar la función de otro campo.
En tales casos, si el desarrollador no comprueba minuciosamente el sistema, es probable que surjan problemas.
Con base en la situación anterior, es necesario realizar una prueba de "anticipación" en la función desde la perspectiva del desarrollador. Una vez que una función ha pasado, qué se debe ingresar, qué se debe generar, qué datos han cambiado, si el cambio cumple con las expectativas, etc.

Recientemente, el negocio de la compañía básicamente se transfirió a Go para su desarrollo, y todo el procesamiento comercial de Go se mejoró gradualmente, y la prueba unitaria de Go también es muy fácil de usar, por lo que haré un pequeño resumen.

1. Base de datos simulada

En las pruebas unitarias, un elemento muy importante es la simulación de la base de datos: la base de datos debe usarse como un estado inicial limpio en cada prueba unitaria y la velocidad de ejecución no debe ser demasiado lenta cada vez.

1. Simulacro de MySQL

Lo que se usa aquí es github.com/dolthub/go-mysql-server, tomando prestado del método de este hermano mayor, cómo realizar una prueba falsa para MySQL

  • Inicialización de base de datos
    en el directorio base de datos
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")
}
  • Inicialización e inyección de fake-mysql.
    En el directorio fake_mysql
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)
   }
}

Al comienzo de la prueba unitaria, InitFakeDb()simplemente llame

func setup() {
    
    
   fake_mysql.InitFakeDb()
}

2. Simulacro de Redis

Lo que se usa aquí es miniredis , y el Cliente Redis coincidente es go-redis/redis/v8llamar a InitTestRedis() aquí para inyectar

// 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. Pruebas unitarias

Después de la comparación, elegí goconvey , un marco de pruebas unitarias
, que es mucho más fácil de usar que las pruebas nativas. goconvey también proporciona muchas funciones útiles:

  • Prueba única anidada de varios niveles
  • rica afirmación
  • Borrar resultados de pruebas individuales
  • Admite prueba de ir nativa

usar

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所需的参数 
内层调用了函数,对返回值进行了断言

La afirmación aquí también puede comparar el valor de retorno So(x, ShouldEqual, 2)
o juzgar la longitud de esta maneraSo(len(resMap),ShouldEqual, 2)

El anidamiento de Convey también puede ser flexible y de varias capas, y puede expandirse como un árbol de múltiples bifurcaciones, lo cual es suficiente para la simulación empresarial.


3. Prueba principal

Agregue un TestMain como entrada unificada para todos los casos

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()
}

Supongo que te gusta

Origin blog.csdn.net/w_monster/article/details/130294825
Recomendado
Clasificación