单元测试
和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日内与变化吧联系。
- 赞助本站
- 微信扫一扫
- 加入Q群
- QQ扫一扫
评论