Meilleures pratiques pour les tests Go en développement | Tests avec des conteneurs Docker

avant-propos

Récemment, j'ai vu de nombreux didacticiels sur les tests de langage Go. Ils ne parlent que de l'utilisation la plus élémentaire des tests et abordent à peine la manière de concevoir un excellent test en développement. Ce blog vous guidera étape par étape pour compléter une excellenteGo-Test

pense

Le langage Go dispose d'un ensemble de systèmes de tests unitaires et de tests de performances, qui peuvent tester rapidement un morceau de code requis avec seulement une petite quantité de code ajouté.

À quoi doit ressembler un bon test ? Il doit être libre de toute contrainte externe ou il n'affectera pas le développement formel une fois le test terminé. Et simuler la situation réelle de développement

pratique

Initialiser le projet

Après avoir ouvert un projet, nous devons d'abord le faire go mod init, et le projet suivant tirera également des bibliothèques tierces

$ go mod init awesomeTest

Afin de simuler le scénario de développement réel, nous utilisons MongoDBpour opérer, et afin d'éviter d'être trop compliqué, nous n'en fixons qu'unkey

Les opérations suivantes impliquent la connaissance de MongoDB. Si vous ne le connaissez pas, ne vous inquiétez pas. La méthode principale n'est pas une base de données spécifique.

Tout d'abord, nous devons l'exécuter localement . Il est MongoDBrecommandé de l'utiliser Docker. Pour ceux qui ne le font pas, vous pouvez consulter mon blog précédent . Déployer MongoDB avec des conteneurs Docker et prendre en charge l'accès à distance

Ensuite, nous devons installer MongoDBles dépendances tierces pour l'opération

$ go get "go.mongodb.org/mongo-driver/mongo"

Ensuite, nous créons les données dont nous avons besoin dans le projet

//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

Au main.gomilieu, nous en avons inséré deux test_idet imprimé les correspondants Après les ObjIDavoir ObjIDrécupérés, nous pouvons effectuer quelques tests simples.

essai primaire

Pour tester, nous devons d'abord implémenter quelques méthodes pour appeler

// 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"),
   }
}

Ensuite, nous pouvons mongodbimplémenter certaines méthodes pour l'objet d'accès aux données. Comme il s'agit d'un didacticiel, nous n'en ferons pas de très compliquées, mais ce sera certainement beaucoup plus compliqué en développement réel, mais les méthodes sont les mêmes.

// 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
}

Lorsque nous passons le contexte et testIDla méthode dans cette méthode, nous MongoDBinterrogeons le correspondant ObjIDet returnsortons. S'il y a une erreur, l'erreur sera imprimée directement et le programme se terminera.

完成这些简单的代码之后就可以开始初级测试,一个非常基础的测试,但可以直接检验方法的正确与否而不需要实例调用

// 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 是一个开源的应用容器引擎,让开发者可以打包他们的应用以及依赖包到一个可移植的镜像中,然后发布到任何流行的 LinuxWindows操作系统的机器上,也可以实现虚拟化。容器是完全使用沙箱机制,相互之间不会有任何接口

关于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
}

完成这一步准备之后我们就可以开始写最终的测试代码

在此之前我们再缕一缕思路,我们的想法是在开始测试之前开启一个MongoDBDocker容器,然后测试结束时自动关闭。测试中呢我们使用表格驱动测试,使用两组数据来测试。开始测试后我们先起一个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来实验一下

image.png

结语

如果有没弄清楚的地方欢迎大家向我提问,我都会尽力解答

这是我的GitHub主页 github.com/L2ncE

欢迎大家Follow/Star/Fork三连

本文正在参加技术专题18期-聊聊Go语言框架

おすすめ

転載: juejin.im/post/7120957545841164295