prefacio
Recientemente, he visto muchos tutoriales sobre las pruebas de lenguaje Go. Solo hablan sobre el uso más básico de las pruebas y apenas mencionan cómo diseñar una prueba excelente en desarrollo. Este blog lo llevará paso a paso para completar una excelenteGo-Test
pensar
El lenguaje Go tiene un conjunto de pruebas unitarias y sistemas de prueba de rendimiento, que pueden probar rápidamente una parte del código requerido con solo una pequeña cantidad de código agregado.
¿Cómo debe ser una buena prueba? Debe estar libre de todas las restricciones externas o no afectará el desarrollo formal una vez que se complete la prueba. Y simular la situación de desarrollo real
práctica
Inicializar el proyecto
Después de abrir un proyecto, primero debemos hacerlo go mod init
, y el proyecto posterior también extraerá bibliotecas de terceros
$ go mod init awesomeTest
Para simular el escenario de desarrollo real, utilizamos MongoDB
para operar, y para evitar ser demasiado complicado, solo configuramos unokey
Las siguientes operaciones implican el conocimiento de MongoDB, si no lo sabes, no te preocupes, el método principal no es una base de datos específica.
En primer lugar, necesitamos ejecutarlo localmente . Se MongoDB
recomienda usarlo Docker
. Para aquellos que no lo hacen, pueden echar un vistazo a mi blog anterior . Implemente MongoDB con contenedores Docker y admita acceso remoto
A continuación, debemos instalar MongoDB
las dependencias de terceros para la operación.
$ go get "go.mongodb.org/mongo-driver/mongo"
Luego creamos los datos que necesitamos en el proyecto.
//main.go
package main
import (
"context"
"fmt"
"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/bson/primitive"
"go.mongodb.org/mongo-driver/mongo"
"go.mongodb.org/mongo-driver/mongo/options"
)
func main() {
c := context.Background()
mc, err := mongo.Connect(c, options.Client().ApplyURI("mongodb://localhost:27017"))
if err != nil {
panic(err)
}
col := mc.Database("awesomeTest").Collection("test")
insertRows(c, col)
}
func insertRows(c context.Context, col *mongo.Collection) {
res, err := col.InsertMany(c, []interface{}{
bson.M{
"test_id": "123",
},
bson.M{
"test_id": "456",
},
})
if err != nil {
panic(err)
}
fmt.Printf("%+v", res)
}
&{InsertedIDs:[ObjectID("62ce8ed4e2aaad4e36242623") ObjectID("62ce8ed4e2aaad4e36
242624")]}
进程 已完成,退出代码为 0
En el main.go
medio, insertamos dos test_id
e imprimimos los correspondientes . Después ObjID
de obtenerlos ObjID
, podemos realizar algunas pruebas simples.
prueba primaria
Para probar, primero debemos implementar algunos métodos para llamar
// mongo/mongo.go
// Mongo定义一个mongodb的数据访问对象
type Mongo struct {
col *mongo.Collection
}
// 使用NewMongo来初始化一个mongodb的数据访问对象
func NewMongo(db *mongo.Database) *Mongo {
return &Mongo{
col: db.Collection("test"),
}
}
A continuación, podemos mongodb
implementar algunos métodos para el objeto de acceso a datos, ya que es un tutorial, no haremos algunos muy complicados, pero definitivamente será mucho más complicado en el desarrollo real, pero los métodos son los mismos.
// mongo/mongo.go
// 将test_id解析为ObjID
func (m *Mongo) ResolveObjID(c context.Context, testID string) (string, error) {
res := m.col.FindOneAndUpdate(c, bson.M{
"test_id": testID,
}, bson.M{
"$set": bson.M{
"test_id": testID,
},
}, options.FindOneAndUpdate().SetUpsert(true).SetReturnDocument(options.After))
if err := res.Err(); err != nil {
return "", fmt.Errorf("cannot findOneAndUpdate: %v", err)
}
var row struct {
ID primitive.ObjectID `bson:"_id"`
}
err := res.Decode(&row)
if err != nil {
return "", fmt.Errorf("cannot decode result: %v", err)
}
return row.ID.Hex(), nil
}
Cuando pasemos el contexto y testID
el método a este método, MongoDB
consultaremos el correspondiente ObjID
y saldremos.Si return
hay un error, el error se imprimirá directamente y el programa finalizará.
完成这些简单的代码之后就可以开始初级测试,一个非常基础的测试,但可以直接检验方法的正确与否而不需要实例调用
// mongo/mongo_test.go
func TestMongo_ResolveAccountID(t *testing.T) {
c := context.Background()
mc, err := mongo.Connect(c, options.Client().ApplyURI("mongodb://localhost:27017"))
if err != nil {
t.Fatalf("cannot connect mongodb: %v", err)
}
m := NewMongo(mc.Database("awesomeTest"))
id, err := m.ResolveObjID(c, "123")
if err != nil {
t.Errorf("faild resolve Obj id for 123: %v", err)
} else {
want := "62ce8ed4e2aaad4e36242623"
if id != want {
t.Errorf("resolve Obj id: want: %q, got: %q", want, id)
}
}
}
上述测试样例先连接了数据库并在测试中新建了一个MongoDB
的数据访问对象,然后将test_id
传了进去并解析出相应的ObjID
,若未解析成功或答案不一致则测试失败
再次思考
完成了上述测试之后我们会想,如果这样进行测试的话会连接外部的数据库,甚至是开发中使用的数据库。一个完美的测试应该不会依赖外界或对外界有造成改变的可能,对此我想到了使用现在非常流行的容器工具Docker
,在测试开始时,我们新建一个MongoDB
的容器,在测试结束之后将其关闭,这样就能完美实现我们的想法
Docker
Docker 是一个开源的应用容器引擎,让开发者可以打包他们的应用以及依赖包到一个可移植的镜像中,然后发布到任何流行的 Linux或Windows操作系统的机器上,也可以实现虚拟化。容器是完全使用沙箱机制,相互之间不会有任何接口
关于Docker
就介绍到这里,如果不会使用的同学同上,可以去看看我Docker
相关的文章
为了实践我们上面所想到操作,我们先新建一个Docker
文件夹进行实验docker/main.go
首先我们拉取在Go语言中操作Docker
的相关第三方包
$ go get -u "github.com/docker/docker"
大概的思路就是先新建一个新的Docker
镜像并给他找一个空的端口运行,预计了一下大概的测试用时以及其他的时间,我们稳妥地让它存活五秒钟,在时间到后立马将其销毁
package main
import (
"context"
"fmt"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/client"
"github.com/docker/go-connections/nat"
"time"
)
func main() {
c, err := client.NewClientWithOpts()
if err != nil {
panic(err)
}
ctx := context.Background()
resp, err := c.ContainerCreate(ctx, &container.Config{
Image: "mongo:latest",
ExposedPorts: nat.PortSet{
"27017/tcp": {},
},
}, &container.HostConfig{
PortBindings: nat.PortMap{
"27017/tcp": []nat.PortBinding{
{
HostIP: "127.0.0.1",
HostPort: "0", //随意找一个空的端口
},
},
},
}, nil, nil, "")
if err != nil {
panic(err)
}
err = c.ContainerStart(ctx, resp.ID, types.ContainerStartOptions{})
if err != nil {
panic(err)
}
fmt.Println("container started")
time.Sleep(5 * time.Second)
inspRes, err := c.ContainerInspect(ctx, resp.ID)
if err != nil {
panic(err)
}
fmt.Printf("listening at %+v\n",
inspRes.NetworkSettings.Ports["27017/tcp"][0])
fmt.Println("killing container")
err = c.ContainerRemove(ctx, resp.ID, types.ContainerRemoveOptions{
Force: true,
})
if err != nil {
panic(err)
}
}
用Go语言简单实现了一下,并不复杂
进阶测试
在思路理清晰之后我们就要对刚才的测试进行改动,所谓进阶测试当然不可能只测试一组数据,我们会使用到表格驱动测试
回到刚才的Docker
操作,我们不可能将上面那么多的代码都放进测试中,所以我们新建一个mongotesting.go
文件将此类函数封装在外部
首先是在Docker
中跑MongoDB
的函数,与上文的区别不大,主要是在创建容器后的操作有所改变
//mongo/mongotesting.go
func RunWithMongoInDocker(m *testing.M) int {
...
containerID := resp.ID
defer func() {
err := c.ContainerRemove(ctx, containerID, types.ContainerRemoveOptions{
Force: true,
})
if err != nil {
panic(err)
}
}()
err = c.ContainerStart(ctx, containerID, types.ContainerStartOptions{})
if err != nil {
panic(err)
}
inspRes, err := c.ContainerInspect(ctx, containerID)
if err != nil {
panic(err)
}
hostPort := inspRes.NetworkSettings.Ports["27017/tcp"][0]
mongoURI = fmt.Sprintf("mongodb://%s:%s", hostPort.HostIP, hostPort.HostPort)
return m.Run()
}
调用了一个defer
使其在return之后就删除掉
接下来是在Docker
中与MongoDB
创建一个新连接的函数
//mongo/mongotesting.go
func NewClient(c context.Context) (*mongo.Client, error) {
if mongoURI == "" {
return nil, fmt.Errorf("mong uri not set. Please run RunWithMongoInDocker in TestMain")
}
return mongo.Connect(c, options.Client().ApplyURI(mongoURI))
}
最后是创建ObjID的函数,也是非常简单的调用
//mongo/mongotesting.go
func mustObjID(hex string) primitive.ObjectID {
ObjID, err := primitive.ObjectIDFromHex(hex)
if err != nil {
panic(err)
}
return ObjID
}
完成这一步准备之后我们就可以开始写最终的测试代码
在此之前我们再缕一缕思路,我们的想法是在开始测试之前开启一个MongoDB
的Docker
容器,然后测试结束时自动关闭。测试中呢我们使用表格驱动测试,使用两组数据来测试。开始测试后我们先起一个MongoDB
的连接,然后新建一个test
数据库并插入两组数据,接下来写测试样例,然后用一个for range
结构跑完所有的数据并验证正确性。
我们将以上的步骤分为三步走
第一步 新建连接,插入数据
//mongo/mongo_test.go
func TestResolveObjID(t *testing.T) {
c := context.Background()
mc, err := NewClient(c)
if err != nil {
t.Fatalf("cannot connect mongodb: %v", err)
}
m := NewMongo(mc.Database("test"))
_, err = m.col.InsertMany(c, []interface{}{
bson.M{
"_id": mustObjID("5f7c245ab0361e00ffb9fd6f"),
"test_id": "testid_1",
},
bson.M{
"_id": mustObjID("5f7c245ab0361e00ffb9fd70"),
"test_id": "testid_2",
},
})
if err != nil {
t.Fatalf("cannot insert initial values: %v", err)
}
...
}
第一步中我们调用了写在mongotesting.go
中的NewClient来创建一个新的连接,并向其中加入了两组数据
第二步 加入样例,准备测试
//mongo/mongo_test.go
func TestResolveObjID(t *testing.T) {
...第一步
cases := []struct {
name string
testID string
want string
}{
{
name: "existing_user",
testID: "testid_1",
want: "5f7c245ab0361e00ffb9fd6f",
},
{
name: "another_existing_user",
testID: "testid_2",
want: "5f7c245ab0361e00ffb9fd70",
},
}
...
}
在第二步中我们使用表格驱动测试的方法放入了两个样例准备进行测试
第三步 遍历样例,使用容器
//mongo/mongo_test.go
func TestResolveObjID(t *testing.T) {
...第一步
...第二步
for _, cc := range cases {
t.Run(cc.name, func(t *testing.T) {
rid, err := m.ResolveObjID(context.Background(), cc.testID)
if err != nil {
t.Errorf("faild resolve Obj id for %q: %v", cc.testID, err)
}
if rid != cc.want {
t.Errorf("resolve Obj id: want: %q; got: %q", cc.want, rid)
}
})
}
}
在这里我们使用了for range
结构遍历了我们所有了样例,将test_id
解析成了ObjID
,成功对应后就会通过测试,反之会报错
接下来是最重要的地方,我们需要使用Docker
就还需要在测试文件中加上最后一个函数
func TestMain(m *testing.M) {
os.Exit(RunWithMongoInDocker(m))
}
在此之后我们的测试就写好了,大家可以打开Docker
来实验一下
结语
如果有没弄清楚的地方欢迎大家向我提问,我都会尽力解答
这是我的GitHub主页 github.com/L2ncE
欢迎大家Follow/Star/Fork三连
本文正在参加技术专题18期-聊聊Go语言框架