Go语言测试说明(翻译)

幸运草 2020年4月22日21:57:56前端框架评论阅读模式
在过去的几个月里,我对许多由Go语言编写的API进行了测试。非常有趣!用Go语言写代码也很有趣。值得注意的是,没有测试就不会有高质量的代码,所以我将在本文中重点探讨测试。

单元测试

和Python一样,Go是一种“开箱即用”的语言,所以自然地它包含了testing包,testing包提供了对自动运行单元测试的支持。以下是其文档摘录:

Go提供了testing包用于支持自动测试。它与“go test”命令协同使用,该命令可以自动执行任何形式的函数
func TestXxx(*testing.T)
Xxx可以是任意文数字组成的字符串(但首字母不能小写),用于识别测试程序。

但是,testing包提供的功能非常简单,所以我引入了名为testify的包,testify提供了测试套件和更友好的断言。

无论你是否使用testing或其他例如testify的第三方包,Go编写单元测试的方式都是将测试代码包含在以_test.go结尾的文件中,并和被测试代码放在同一目录。例如,有个名为customers.go的文件用于处理客户管理业务逻辑,你可以对这些代码编写单元测试,并将单元测试代码放进名为customers_test.go的文件中,并和customers.go在一个目录。然后,当对该目录执行“go test”命令的时候,单元测试代码将自动运行。实际上,“go test”找出所有名为*_test.go的文件并执行。你可以在“How to Write Go Code”一文的Testing章节找到更多关于Go单元测试细节。

集成测试

这里我要给出一些怎样组织集成测试的事例。让我们再次以测试处理客户管理的API为例。根据目的,集成测试将通过HTTP从外部访问API端点。与单元测试不同,单元测试是从内部测试API处理程序的业务逻辑。如上面所说,集成测试代码也要和API代码存放在相同的包中。

对于集成测试,我通常会为测试对象创建相应的目录,例如core-api。然后在那里创建main.go文件,用于设置测试所需的常量:

package main
import (
  “fmt”
)
const API_VERSION = “v2”
const API_HOST = “myapi.example.com”
const API_PORT = 8000
const API_PROTO = “http”
const API_INIT_KEY = “some_init_key”
const API_SECRET_KEY = “some_secret_key”
const TEST_PHONE_NUMBER = “+15555550000”
const DEBUG = true
func init() {
  fmt.Printf(“API_PROTO:%s; API_HOST:%s; API_PORT:%dn”, API_PROTO, API_HOST, API_PORT)
}
func main() {
}

对于与customer API相关的集成测试,我创建了名为customer_test.go的文件,内容如下:

package main
import (
  “fmt”
  “testing”
  “github.com/stretchr/testify/assert”
  “github.com/stretchr/testify/suite”
)
// Define the suite, and absorb the built-in basic suite
// functionality from testify — including a T() method which
// returns the current testing context
type CustomerTestSuite struct {
  suite.Suite
  apiURL string
  testPhoneNumber string
}
// Set up variables used in all tests
// this method is called before each test
func (suite *CustomerTestSuite) SetupTest() {
  suite.apiURL = fmt.Sprintf(“%s://%s:%d/%s/customers”, API_PROTO, API_HOST, API_PORT, API_VERSION)
  suite.testPhoneNumber = TEST_PHONE_NUMBER
}
// Tear down variables used in all tests
// this method is called after each test
func (suite *CustomerTestSuite) TearDownTest() {
}
// In order for ‘go test’ to run this suite, we need to create
// a normal test function and pass our suite to suite.Run
func TestCustomerTestSuite(t *testing.T) {
  suite.Run(t, new(CustomerTestSuite))
}

借助testify包,我就可以定义测试套件了,这是一个包含testify suite.Suite匿名字段,名为CustomerTestSuite的结构体。Go语言通过组合替代继承,因此在测试套件中嵌入suite.Suite将使我可以在CustomerTestSuite中定义SetupTest和TearDownTest方法。在SetupTest中为所有测试函数做统一设置(在每个测试函数被执行前调用),并在TearDownTest中为所有测试函数做统一销毁(在每个测试函数被执行后调用)。

上面的例子中,SetupTest设置了一些变量,这样就能在每个测试函数中使用这些变量。下面是测试函数的例子:

func (suite *CustomerTestSuite) TestCreateCustomerNewEmailPhone(){
  url := suite.apiURL
  random_email_addr := fmt.Sprintf(“test-user%[email protected]”, common.RandomInt(1, 1000000))
  phone_num := suite.testPhoneNumber
  status_code, json_data := create_customer(url, phone_num, random_email_addr)
  customer_id := get_nested_item_property(json_data, “customer”, “id”)
  assert_success_response(suite.T(), status_code, json_data)
  assert.NotEmpty(suite.T(), customer_id, “customer id should not be empty”)
}

对于实际HTTP调用后端API的测试过程,我在utils.go的create_customer函数中定义:

func create_customer(url, phone_num, email_addr string) (int, map[string]interface{}) {
  fmt.Printf(“Sending request to %sn”, url)
  payload := map[string]string{
    “phone_num”: phone_num,
    “email_addr”: email_addr,
  }
  ro := &grequests.RequestOptions{}
  ro.JSON = payload
  var resp *grequests.Response  
  resp, _ = grequests.Post(url, ro)
  var json_data map[string]interface{}
  status_code := resp.StatusCode
  err := resp.JSON(&json_data)
  if err != nil {
    fmt.Println(“Unable to coerce to JSON”, err)
    return 0, nil
  }
  return status_code, json_data
}

注意,我使用了grequests包,它是Python Requests库的Go语言实现。通过使用grequests,我可以用优雅的方式封装HTTP请求和响应,并轻松处理JSON。

回到TestCreateCustomerNewEmailPhone测试函数,一旦收到了调用创建客户API的响应,就调用另一个名为assert_success_response的协助函数,该函数使用了testify的assert包验证HTTP响应代码是否为200,并具体确认返回的JSON响应参数(如error_msg, error_code, req_id)是否和我们期望的一样:

func assert_success_response(testobj *testing.T, status_code int, json_data map[string]interface{}) {
  assert.Equal(testobj, 200, status_code, “HTTP status code should be 200”)
  assert.Equal(testobj, 0.0, json_data[“error_code”], “error_code should be 0”)
  assert.Empty(testobj, json_data[“error_msg”], “error_msg should be empty”)
  assert.NotEmpty(testobj, json_data[“req_id”], “req_id should not be empty”)
  assert.Equal(testobj, true, json_data[“success”], “success should be true”)
}

为实际运行集成测试,我在包含测试文件的目录中执行了“go test”命令。

这个模式用于应对日益增长的API端点集成测试需求颇有价值。

测试覆盖率

在Go语言“开箱即用”系列工具中,有一部分是测试覆盖工具。要使用它,需要运用多个覆盖测试参数来运行“go test”。下面是一个用于生成测试覆盖率的shell脚本:

#!/bin/bash
#
# Run all of our go unit-like tests from each package
#
CTMP=$GOPATH/src/core_api/coveragetmp.out
CREAL=$GOPATH/src/core_api/coverage.out
CMERGE=$GOPATH/src/core_api/merged_coverage.out
set -e
set -x
cp /dev/null $CTMP
cp /dev/null $CREAL
cp /dev/null $CMERGE
go test -v -coverprofile=$CTMP -covermode=count -parallel=9 ./auth
cat $CTMP > $CREAL
go test -v -coverprofile=$CTMP -covermode=count -parallel=9 ./customers
cat $CTMP |tail -n+2 >> $CREAL
#
# Finally run all the go integration tests
#
go test -v -coverprofile=$C -covermode=count -coverpkg=./auth,./customers ./all_test.go
cat $CTMP |tail -n+2 >> $CREAL
rm $CTMP
#
# Merge the coverage report from unit tests and integration tests
#
cd $GOPATH/src/core_api/
cat $CREAL | go run ../samples/mergecover/main.go >> $CMERGE
#
set +x
echo “You can run the following to view the full coverage report!::::”
echo “go tool cover -func=$CMERGE”
echo “You can run the following to generate the html coverage report!::::”
echo “go tool cover -html=$CMERGE -o coverage.html”

上面bash脚本的第一部分在运行“go test”时带入covermode=count参数,将统计每个子包(auth、customers等)。然后再将coverprofile输出文件(CTMP)合并到单个文件(CREAL)中。

第二部分通过带入covermode=count、coverpkg=[逗号分隔的包列表]参数运行“go test”, 对文件all_test.go进行集成测试。该文件启动一个HTTP服务器暴露API,然后在集成测试目录中调用“go test”访问API。

最后,通过运行mergecover工具,将来自单元测试和集成测试的覆盖率数据合并到CMERGE文件中。

至此,就能通过go tool cover -html=$CMERGE -o coverage.html命令生成html文件了,并能在浏览器中进一步查看coverage.html。目标是每个包的测试覆盖率超过80%。

特别声明:以上文章内容仅代表作者本人观点,不代表变化吧观点或立场。如有关于作品内容、版权或其它问题请于作品发表后的30日内与变化吧联系。

  • 赞助本站
  • 微信扫一扫
  • weinxin
  • 加入Q群
  • QQ扫一扫
  • weinxin
幸运草
Go语言接口规则 前端框架

Go语言接口规则

Go语言接口规则 接口是一个或多个方法签名的集合。任何类型的方法集中只要拥有该接口对应的全部方法签名。就表示它 "实现" 了该接口,无须在该类型上显式声明实现了哪个接口。对应方法,是指有相同名称、参数...
Go语言中处理 HTTP 服务器 前端框架

Go语言中处理 HTTP 服务器

1 概述 包 net/http 提供了HTTP服务器端和客户端的实现。本文说明关于服务器端的部分。 快速开始: package main import (   "log"   "net/http" )...

发表评论