/ go

Go + Docker API服务开发和部署 - 测试篇

本文是 Go + Docker API服务开发和部署 文章系列的“测试篇”,主要讲解如何使用Goconvey来进行单元测试、Model测试和API测试。

  1. Goconvey介绍

Goconvey介绍

Go内置了“testing”包用来做单元测试和性能测试。Goconvey 核心也是使用“testing”包,但增加了行为测试、丰富的测试断言、WebUI、自动监测更新等功能,有效提升了测试开发效率和体验。

单元测试

先来看看不依赖外部环境的单元测试如何编写,借此了解Goconvey的用法。

单元测试示例:

// test/add.go

package test

import (
  "testing"

  . "github.com/smartystreets/goconvey/convey"
)

func TestSpec(t *testing.T) {
  Convey("Given some integer with a starting value", t, func() {
    x := 1

    Convey("When the integer is incremented", func() {
      x++

      Convey("The value should be greater by one", func() {
        So(x, ShouldEqual, 2)
      })
    })
  })
}

执行 goconvey ,将运行测试代码,并自动打开浏览器显示测试结果。只要goconvey命令不被终端,一旦代码有更新,将自动运行测试,浏览器里的测试结果也会同步更新。在WebUI里也可以控制测试的运行和中断。
单元测试结果

因为Goconvey跟Go自带的“testing”包兼容,因此上面的测试也可以使用 go test 命令来运行。

➜  test git:(master) ✗ go test -v
=== RUN   TestSpec

  Given some integer with a starting value
    When the integer is incremented
      The value should be greater by one ✔


1 total assertion

--- PASS: TestSpec (0.00s)
PASS
ok    zaiqiuchang.com/server/test 0.012s

Model测试

单元测试适合哪些不依赖外部环境的业务逻辑和算法测试,如果代码依赖外部环境,需要通过Mock的方式来模拟外部服务接口。对于Model层,以及本应用里采用的 MVCS 模式里提出的Service层,这些严重依赖数据库的代码,不建议使用Mock方式。一是数据库服务本身也是测试的一部分,二是很难完全去Mock数据库服务的所有接口,费时费力且效果也不好。以前要测试访问数据库的代码代价很大,需要手动去部署和配置用于测试的数据库服务。如今利用Docker,我们可以很快的启动一个提供数据库服务的容器,并在测试完成后立即销毁这个容器。启动和销毁速度都在秒级,因此测试体验接近于单元测试。

Main Test完成测试数据库的创建和销毁:

// test/model/main_test.go

package model

import (
  "os"
  "testing"
  "time"

  "github.com/spf13/viper"

  "zaiqiuchang.com/server/models"
  "zaiqiuchang.com/server/test"
)

func TestMain(m *testing.M) {
  // 使用Docker API启动一个测试用的MongoDB服务
  addrs, ctn, err := test.StartMongodb(10, 1*time.Second)
  if err != nil {
    panic(err)
  }
  // 替换配置项里的连接地址为测试服务地址
  viper.Set("mongodb.zqc", map[string]interface{}{
    "addrs": addrs,
  })

  // 在新的数据库里创建索引
  err = models.CreateDBIndexes("zqc", "zqc", "", -1)

  result := m.Run()

  // 测试完后,销毁测试用的MongoDB服务
  test.RemoveMongodb(ctn)

  os.Exit(result)
}

关于如何使用Docker API来创建和消耗测试数据库服务,将在“部署篇”里讲解。

Model测试示例:

// test/model/user_test.go

package model

import (
  "testing"

  . "github.com/smartystreets/goconvey/convey"

  "zaiqiuchang.com/server/models"
)

func TestEmptyUserColl(t *testing.T) {
  Convey("Insert a user to collection", t, func() {
    uc, err := models.NewUserColl()
    So(err, ShouldBeNil)
    err = uc.Insert(models.User{
      Username: "jaggerwang",
      Password: "198157",
      Nickname: "jag",
      Gender:   "m",
      Mobile:   "18683420507",
    })
    So(err, ShouldBeNil)

    Convey("Empty collection", func() {
      info, err := uc.RemoveAll(nil)
      So(err, ShouldBeNil)
      So(info.Removed, ShouldEqual, 1)
    })

    Reset(func() {
      models.EmptyDB("zqc", "zqc", "")
    })
  })
}

Model测试结果:
Model测试结果

API测试

API测试,亦即集成测试,对于API服务来说是最应该做的,因为运行服务的目的就是为了对外提供API访问。
Go的“net/http/httptest”包提供了无需启动一个真实的API服务就可以测试handler的方法,非常的方便。但这种方式并不能测试完整的API服务。比如一些中间件的加载是在服务启动时加载,这种方式下由于没有启动服务,自然也就没有加载中间件,导致一些逻辑无法覆盖到。
为了进行API测试,除了需要启动API服务本身,还要启动API服务依赖的其它服务,包括数据库服务、缓存服务、消息队列服务等。通过Docker Compose我们能够做到一个命令就将整套服务启动起来,并且相互之间可以互相访问。启动整套服务将在“部署篇”里讲解,这里我们先看API测试如何编写。

为了提升测试速度,API测试需要的整套服务是手动预先启动起来,而不是每次运行测试都重新部署。但每次运行测试前需要先清空数据库。

启动API测试用的服务的脚本:

#!/usr/bin/env bash

source ./common.sh

./build.sh
if [[ $? != 0 ]]; then
  exit 1
fi

./docker-build.sh
if [[ $? != 0 ]]; then
  exit 1
fi

log INFO "docker compose begin ..."
docker-compose -p zqctest -f docker-compose.yml -f docker-compose.test.yml up -d
log INFO "docker compose end"

log INFO "create db indexes begin ..."
docker-compose -p zqctest exec server zqc db createindexes
log INFO "create db indexes end"

exit 0

首先build代码,然后build应用镜像,再使用 docker-compose 命令部署整套服务,最后在测试服务里创建数据库索引。

Main Test里需要清空数据库和创建索引:

// test/controller/main_test.go

package controller

import (
  "os"
  "testing"

  "github.com/spf13/viper"

  "zaiqiuchang.com/server/models"
)

func TestMain(m *testing.M) {
  // 修改数据库连接地址为API测试服务里的数据库服务,API测试本是不直接访问数据库,这里是为了后面清空数据库和创建索引做准备
  viper.Set("mongodb.zqc", map[string]interface{}{
    "addrs": "127.0.0.1:27019",
  })

  // 清空数据库
  models.EmptyDB("zqc", "zqc", "")

  // 创建索引
  createDBIndexes()

  result := m.Run()

  os.Exit(result)
}

API测试示例:

// test/controller/account_test.go

package controller

import (
  "net/url"
  "testing"

  . "github.com/smartystreets/goconvey/convey"

  "zaiqiuchang.com/server/models"
  "zaiqiuchang.com/server/services"
)

func TestRegisterUser(t *testing.T) {
  Convey("Given some user info", t, func() {
    username := "jaggerwang"
    password := "198157"
    nickname := "jag"
    gender := "m"
    mobile := "18683420507"

    Convey("Register a user", func() {
      f := make(url.Values)
      f.Set("username", username)
      f.Set("password", password)
      f.Set("nickname", nickname)
      f.Set("gender", gender)
      f.Set("mobile", mobile)
      result := postResult("/account/register", f, "")

      So(result["code"].(float64), ShouldEqual, services.ErrCodeOk)
      user := result["data"].(map[string]interface{})["user"].(map[string]interface{})
      So(user["username"].(string), ShouldEqual, username)
      So(user["nickname"].(string), ShouldEqual, nickname)
      So(user["gender"].(string), ShouldEqual, gender)
      So(user["mobile"].(string), ShouldEqual, mobile)
      So(user, ShouldNotContainKey, "password")
      So(user, ShouldNotContainKey, "salt")
    })

    Reset(func() {
      models.EmptyDB("zqc", "zqc", "")
    })
  })
}

// 测试编辑用户资料
func TestEditUser(t *testing.T) {
  // ...
}

// ...

API测试结果:
API测试结果