golang标准库中和测试有关的基础设施有:testing包及基于它的net/http/httptest包、go test 命令。
testing包要求测试文件的名字以 _test.go后缀结尾,一般测试文件名字和被测试源码文件是对应的(如 server_test.go 文件测试的是 server.go 文件中的函数或功能),并且测试文件和被测试文件位于同一个包内。
在测试文件内部使用的测试函数,必须是以下格式的函数(这里指的是功能测试,基准测试函数格式见后),函数内部可以使用 Error、Errorf、Fail、FailNow、Log、Logf、Skip等方法表示测试失败、记录到日志或跳过测试(Fatal = Log + FailNow,Fatalf=Logf + FailNow):
func TestXxx(t *testing.T) {
// ............
t.Error(..)
// ............
t.Fail(..)
// ............
t.Skip(..)
}
当用户在终端执行 go test 命令时,所有 TestXxx 测试函数就会被执行。
下面的测试例子,建立在项目 jsonparse 基础上(golang web学习随便记7-XML、JSON、Web服务_sjg20010414的博客-CSDN博客 decoder方式解析),把解码的功能打包到函数 decode 中,然后函数 decode 作为我们测试的目标函数。
// ......................................................
func decode(filename string) (post Post, err error) {
jsonFile, err := os.Open(filename) // 文件名改成参数
if err != nil {
fmt.Println("打开JSON文件出错:", err)
return
}
defer jsonFile.Close()
decoder := json.NewDecoder(jsonFile)
for {
// var post Post
// err := decoder.Decode(&post)
err = decoder.Decode(&post)
if err == io.EOF {
break
}
if err != nil {
fmt.Println("解码JSON出错:", err)
return
}
// fmt.Println(post) // 打印输出非常耗时
}
return // 返回
}
func main() {
_, err := decode("post.json") // 改成函数调用
if err != nil && err != io.EOF {
fmt.Println("Error: ", err)
}
}
创建文件 main_test.go,对 main.go 文件进行测试(主要就是测试 decode 函数,因此测试函数命名成了 TestDecode):
package main
import (
"io"
"testing"
)
func TestDecode(t *testing.T) {
post, err := decode("post.json")
if err != nil && err != io.EOF {
t.Error(err)
}
if post.Id != 1 {
t.Error("错误的 id, 期望得到 1 但得到的值为 ", post.Id)
}
if post.Content != "你好,Golang" {
t.Error("错误的内容, 期望得到 '你好,Golang' 但得到的为 ", post.Content)
}
}
func TestEncode(t *testing.T) {
t.Skip("暂时跳过编码函数的测试")
}
运行测试用例的方式类似如下:(显然,我们可以从 VSCode等 IDE 直接点击函数左侧三角形来运行单个测试函数)
sjg@sjg-PC:~/go/src/jsonparse$ go test
{1 你好,Golang {2 张三} [{1 C++才是好语言 {3 李四}} {2 Rust比C++好 {4 王五}}]}
PASS
ok jsonparse 0.001s
sjg@sjg-PC:~/go/src/jsonparse$ go test -v -cover
=== RUN TestDecode
{1 你好,Golang {2 张三} [{1 C++才是好语言 {3 李四}} {2 Rust比C++好 {4 王五}}]}
--- PASS: TestDecode (0.00s)
=== RUN TestEncode
main_test.go:22: 暂时跳过编码函数的测试
--- SKIP: TestEncode (0.00s)
PASS
jsonparse coverage: 61.1% of statements
ok jsonparse 0.003s
我们添加一个测试函数,用来模拟非常耗时的测试过程:
func TestLongRunningTest(t *testing.T) {
if testing.Short() {
t.Skip("在 短时模式 下跳过长时间运行的测试")
}
time.Sleep(10 * time.Second)
}
上述测试函数中,用 testing.Short() 判断测试是否处于短时模式,如果处于短时模式,就跳过当前测试函数。分别用 go test -v -cover 和 go test -v -cover -short 测试就能体会这种差别。
对于不存在依赖的单元,golang可以实现并行地测试(在测试函数中调用 t.Parallel()即可)。下面的例子用3个耗时分别为1秒、2秒、3秒的测试函数来演示并行运行测试的方法,编写 parallel_test.go 如下:
package main
import (
"testing"
"time"
)
func TestParallelCostOneSec(t *testing.T) {
t.Parallel()
time.Sleep(1 * time.Second)
}
func TestParallelCostTwoSec(t *testing.T) {
t.Parallel()
time.Sleep(2 * time.Second)
}
func TestParallelCostThreeSec(t *testing.T) {
t.Parallel()
time.Sleep(3 * time.Second)
}
分别用三条命令运行测试如下(观察总耗时): -parallel 参数可以指定最多并行运行几个测试用例
sjg@sjg-PC:~/go/src/jsonparse$ go test -v -short
........................................
=== CONT TestParallelCostOneSec
=== CONT TestParallelCostTwoSec
=== CONT TestParallelCostThreeSec
--- PASS: TestParallelCostOneSec (1.00s)
--- PASS: TestParallelCostTwoSec (2.00s)
--- PASS: TestParallelCostThreeSec (3.00s)
PASS
ok jsonparse 3.002s
sjg@sjg-PC:~/go/src/jsonparse$ go test -v -short -parallel 3
................................................
=== CONT TestParallelCostOneSec
=== CONT TestParallelCostTwoSec
=== CONT TestParallelCostThreeSec
--- PASS: TestParallelCostOneSec (1.00s)
--- PASS: TestParallelCostTwoSec (2.00s)
--- PASS: TestParallelCostThreeSec (3.00s)
PASS
ok jsonparse 3.003s
sjg@sjg-PC:~/go/src/jsonparse$ go test -v -short -parallel 2
............................................
=== CONT TestParallelCostOneSec
=== CONT TestParallelCostTwoSec
--- PASS: TestParallelCostOneSec (1.00s)
=== CONT TestParallelCostThreeSec
--- PASS: TestParallelCostTwoSec (2.00s)
--- PASS: TestParallelCostThreeSec (3.00s)
PASS
ok jsonparse 4.004s
和单元测试一样,基准测试文件也是以 _test.go 为后缀;和单元测试不同的是,每个基准测试函数格式如下:
func BenchmarkDecode(b *testing.B) {
for i := 0; i < b.N; i++ { // 执行 b.N 次作为基准
// 待测试的操作
}
}
然后运行go test命令时,添加 -bench
编写如下bench_test.go 文件:
package main
import "testing"
func BenchmarkDecode(b *testing.B) {
for i := 0; i < b.N; i++ {
decode("post.json")
}
}
用命令 sjg@sjg-PC:~/go/src/jsonparse$ go test -v -cover -short -bench . 运行。
注意:在基准测试中,测试用例的迭代次数是由golang自己确定的,用户只能限制运行时间,不能精确控制迭代次数。默认golang会迭代足够多的次数来获得比较准确的结果。
注意:上面的这条命令,功能测试也会一并执行,如果要跳过功能测试,方法是用 -run 参数指定一个不存在的功能测试,从而功能测试被跳过,即 go test -run notexist -bench .
sjg@sjg-PC:~/go/src/jsonparse$ go test -run notexist -bench .
goos: linux
goarch: amd64
pkg: jsonparse
cpu: Intel(R) Core(TM) i7-10510U CPU @ 1.80GHz
BenchmarkDecode-8 100562 10268 ns/op
PASS
ok jsonparse 1.156s
基准测试还能帮我们判断哪个函数运行更快(当然,判断完成同一件事哪个更快才有意义)。我们先把 golang web学习随便记7-XML、JSON、Web服务_sjg20010414的博客-CSDN博客
中 Unmarshal 方式的 json 解码部分重构成 unmarshal 函数放入 main.go(书作者的decode函数是只解码一个post的,少掉判断和循环,因此时间最短。书作者的decode我的代码里命名为decode2):
func decode2(filename string) (post Post, err error) {
jsonFile, err := os.Open(filename)
if err != nil {
fmt.Println("Error opening JSON file:", err)
return
}
defer jsonFile.Close()
decoder := json.NewDecoder(jsonFile)
err = decoder.Decode(&post)
if err != nil {
fmt.Println("Error decoding JSON:", err)
return
}
return
}
func unmarshal(filename string) (post Post, err error) {
jsonFile, err := os.Open(filename) // 文件名改成参数
if err != nil {
fmt.Println("打开JSON文件出错:", err)
return
}
defer jsonFile.Close()
jsonData, err := ioutil.ReadAll(jsonFile)
if err != nil {
fmt.Println("读取JSON数据出错:", err)
return
}
// var post Post
json.Unmarshal(jsonData, &post)
// fmt.Println(post)
return // 返回
}
然后在 bench_test.go 文件中添加如下测试函数:
func BenchmarkDecode2(b *testing.B) {
for i := 0; i < b.N; i++ {
decode2("post.json")
}
}
func BenchmarkUnmarshal(b *testing.B) {
for i := 0; i < b.N; i++ {
unmarshal("post.json")
}
}
运行结果如下:(该结果从侧面证明,对于不大的JSON数据,用 Unmarshal 就可以了)
sjg@sjg-PC:~/go/src/jsonparse$ go test -run notexist -bench .
goos: linux
goarch: amd64
pkg: jsonparse
cpu: Intel(R) Core(TM) i7-10510U CPU @ 1.80GHz
BenchmarkDecode-8 105285 10177 ns/op
BenchmarkDecode2-8 146533 7826 ns/op
BenchmarkUnmarshal-8 146584 7885 ns/op
PASS
ok jsonparse 3.660s
前面的测试,属于通用的单元测试和基准测试,对于 golang web,经常需要的是 HTTP 测试,即模拟客户发起HTTP请求,获取模拟服务器返回的HTTP响应。下面的代码,用了前面编写的web服务的例子(golang web学习随便记7-XML、JSON、Web服务_sjg20010414的博客-CSDN博客Web服务)。
在web服务项目中,创建 main_test.go 测试文件:
package main
import (
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
)
func TestHandleGet(t *testing.T) {
mux := http.NewServeMux() // 用于运行测试的多路复用器
mux.HandleFunc("/post/", handleRequest) // 绑定待测 handler
writer := httptest.NewRecorder() // 服务器响应都“录制”到writer
request, _ := http.NewRequest("GET", "/post/1", nil)
mux.ServeHTTP(writer, request) // request请求后,响应的数据 writer 负责
if writer.Code != 200 {
t.Errorf("响应代码 %v", writer.Code)
}
var post Post
json.Unmarshal(writer.Body.Bytes(), &post)
if post.Id != 1 {
t.Error("未能获取 JSON post")
}
}
对于测试单个请求,上面代码中的多路复用器似乎无必要,它在后面的多个请求在一起的测试中才体现价值。上面的测试代码表明,HTTP测试时,测试文件自己就是服务器,只是这个服务器比较特殊,它会把请求后服务器的响应都录制下来,然后对录制的结果进行检验。
记得先从docker启动mariadb 10.3服务器,然后 go test 运行该测试:(500是数据库服务器没开时的结果)
sjg@sjg-PC:~/go/src/webservice$ go test
--- FAIL: TestHandleGet (0.00s)
main_test.go:19: 响应代码 500
main_test.go:24: 未能获取 JSON post
FAIL
exit status 1
FAIL webservice 0.003s
sjg@sjg-PC:~/go/src/webservice$ go test
PASS
ok webservice 0.005s
我们在 main_test.go 中添加一个测试函数:
func TestHandlePut(t *testing.T) {
mux := http.NewServeMux() // 用于运行测试的多路复用器
mux.HandleFunc("/post/", handleRequest) // 绑定待测 handler
writer := httptest.NewRecorder() // 服务器响应都“录制”到writer
json := strings.NewReader(`{"content":"C++老矣,尚能打否","author":"李四"}`)
request, _ := http.NewRequest("PUT", "/post/1", json)
mux.ServeHTTP(writer, request)
if writer.Code != 200 {
t.Errorf("响应代码 %v", writer.Code)
}
}
下面的命令可以单独测试这个函数(然而,你要验证数据改了没有,只能去数据库里看,或者修改前面的测试来验证数据):
sjg@sjg-PC:~/go/src/webservice$ go test -run "TestHandlePut"
PASS
ok webservice 0.005s
从 TestHandleGet 和 TestHandlePut 我们可以发现有不少代码是重复的,如果有更多测试函数,重复带来的累赘会更明显。对此,testing包支持用 TestMain 函数和setUp、tearDown机制来对测试工作进行预设和拆卸工作。典型的 TestMain 函数结构如下:
func TestMain(m *testing.M) {
setUp()
code := m.Run()
tearDown()
os.Exit(code)
}
func setUp() {
// 初始化或预设一些东西
}
func tearDown() {
// 拆卸一些东西
}
使用 TestMain 机制,我们可以改写 main_test.go 如下:
// ..........................................
var mux *http.ServeMux
var writer *httptest.ResponseRecorder
func TestMain(m *testing.M) {
setUp()
code := m.Run()
os.Exit(code)
}
func setUp() {
mux = http.NewServeMux()
mux.HandleFunc("/post/", handleRequest)
writer = httptest.NewRecorder()
}
func TestHandleGet(t *testing.T) {
// mux := http.NewServeMux() // 用于运行测试的多路复用器
// mux.HandleFunc("/post/", handleRequest) // 绑定待测 handler
// writer := httptest.NewRecorder() // 服务器响应都“录制”到writer
// ................................
}
func TestHandlePut(t *testing.T) {
// mux := http.NewServeMux() // 用于运行测试的多路复用器
// mux.HandleFunc("/post/", handleRequest) // 绑定待测 handler
// writer := httptest.NewRecorder() // 服务器响应都“录制”到writer
// ................................
}
前面的 GET、PUT 测试展示的测试功能,只能测试web服务的多路复用器和处理器,差不做相当于 controller 层,它无法覆盖数据层(相当于Model和Service)。
当测试不方便使用实际的对象、结构或者函数时,我们需要测试替身(test double)来提高被测代码的独立性。例如,你并不想让单元测试用例依赖真实的数据库,你也不想邮件发送代码真的在每次测试时发送邮件。
测试替身的麻烦之处在于如果没有在设计程序时考虑过测试替身这回事,那么很可能在现实测试中使用这项技术。换句话说,不能未雨绸缪,就会导致程序(至少某些部分)无法测试。
实现测试替身的一种设计方法是使用依赖注入(dependency injection)设计模式:这种模式通过向被调用的对象、结构或者函数传入依赖关系,然后由依赖关系代替被调用者执行实际的操作,从而解耦软件中的两个或多个层。在golang中,传入的依赖关系通常会设计成接口类型。
下面的图,展示了使用测试替身的Web服务(实现和 golang web学习随便记7-XML、JSON、Web服务_sjg20010414的博客-CSDN博客一样的功能,但可以不依赖数据库进行测试):
创建接口Text,接口约定函数包含了原来模型 Post 所支持的功能,接口本身不依赖数据库连接db。正常工作的模型 Post 和作为测试替身的模型FakePost都实现Text接口,原来依赖 Post 模型的 handleGet,调用有关功能(GetPost(id))时调用接口 Text 的函数而非具体的模型(Post或FakePost)的函数,也就是说,插入了抽象的Text接口,延迟确定具体调用(或者依赖)的模型(这也是多态)。本质上模型 Post 函数还是需要数据库连接的,只是,在测试替身设计模式中,它不再是全局变量,而是作为Post类型的成员存在,并且,它在一开始(main函数)中就生成并注入,然后向后传递。
创建项目 webserviceTestDouble,先改造原来的代码 data.go 和 main.go
package main
import (
"database/sql"
"log"
_ "github.com/go-sql-driver/mysql"
)
type Text interface { // 添加接口
getPost(id int) (err error) // 这些方法不直接使用,是通过类型使用,开头改小写
create() (err error)
update() (err error)
delete() (err error)
posts(limit int) (posts []Text, err error)
}
type Post struct {
Db *sql.DB // 添加成员
Id int `json:"id"`
Content string `json:"content"`
Author string `json:"author"`
}
// var Db *sql.DB
// func init() { // 此 init 不显式调用,自动隐式调用,实现初始化全局变量 Db
// var err error
// Db, err = sql.Open("mysql", "gwp:dbpassword@tcp(172.17.0.1:3306)/gwp?charset=utf8mb4,utf8")
// if err != nil {
// panic(err)
// }
// }
func (post *Post) posts(limit int) (posts []Text, err error) { // 改成接收者的函数,改小写
rows, err := post.Db.Query("SELECT id, content, author FROM posts LIMIT ?", limit) // Db 改成 post的成员
if err != nil {
return
}
for rows.Next() { // 用循环遍历多行结果集
post := Post{}
err = rows.Scan(&post.Id, &post.Content, &post.Author) // Scan(..) 将结果集当前列值绑定到变量
if err != nil {
return
}
posts = append(posts, &post) // 结果依次放入切片 posts
}
rows.Close() // 关闭结果集,清理内存
return
}
func (post *Post) getPost(id int) (err error) { // 改成接收者的函数
// post = Post{}
err = post.Db.QueryRow("SELECT id, content, author FROM posts WHERE id = ?", id). // Db 改成 post的成员
Scan(&post.Id, &post.Content, &post.Author) // pgsql 可以 RETURNING id
return
}
func (post *Post) create() (err error) {
sql := "INSERT INTO posts (content, author) VALUES (?, ?)"
stmt, err := post.Db.Prepare(sql) // Db 改成 post的成员
if err != nil {
log.Fatal(err)
return
}
defer stmt.Close()
// err = stmt.QueryRow(post.Content, post.Author).Scan(&post.Id) // pgsql 可以一步设置 post.id
if result, err := stmt.Exec(post.Content, post.Author); err != nil { // Exec(..)返回执行结果
log.Fatal(err)
} else {
if last_insert_id, err := result.LastInsertId(); err != nil {
log.Fatal(err)
} else {
post.Id = int(last_insert_id) // Mariadb/MySQL应该支持执行结果的 LastInsertId()
}
}
return
}
func (post *Post) update() (err error) {
_, err = post.Db.Exec("UPDATE posts SET content = ?, author = ? WHERE id = ?", //Db 改成 post的成员
post.Content, post.Author, post.Id)
return
}
func (post *Post) delete() (err error) {
_, err = post.Db.Exec("DELETE FROM posts WHERE id = ?", post.Id) // Db 改成 post的成员
return
}
package main
import (
"database/sql"
"encoding/json"
"net/http"
"path"
"strconv"
_ "github.com/go-sql-driver/mysql"
)
func main() {
var err error // 创建数据库连接池的代码放到 main (相当于原 data.go init()代码)
db, err := sql.Open("mysql", "gwp:dbpassword@tcp(172.17.0.1:3306)/gwp?charset=utf8mb4,utf8")
if err != nil {
panic(err)
}
server := http.Server{
Addr: "127.0.0.1:8088",
}
http.HandleFunc("/post/", handleRequest(&Post{Db: db})) // 注入依赖 db
http.HandleFunc("/posts/", handleList(&Post{Db: db})) // 注入依赖 db
server.ListenAndServe()
}
func handleRequest(t Text) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) { // 串联 handler 通过接口 t 传递依赖db
var err error
switch r.Method {
case "GET":
err = handleGet(w, r, t)
case "POST":
err = handlePost(w, r, t)
case "PUT":
err = handlePut(w, r, t)
case "DELETE":
err = handleDelete(w, r, t)
}
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
}
}
func handleGet(w http.ResponseWriter, r *http.Request, post Text) (err error) { // 添加接口型参数
id, err := strconv.Atoi(path.Base(r.URL.Path)) // 路径中最后一个元素转换成整数
if err != nil {
return
}
// post, err := GetPost(id)
err = post.getPost(id) // 修改调用形式
if err != nil {
return
}
output, err := json.MarshalIndent(&post, "", "\t")
if err != nil {
return
}
w.Header().Set("Content-Type", "application/json")
w.Write(output)
return
}
func handlePost(w http.ResponseWriter, r *http.Request, post Text) (err error) {
len := r.ContentLength
body := make([]byte, len)
r.Body.Read(body)
// var post Post
json.Unmarshal(body, &post)
// err = post.Create()
err = post.create()
if err != nil {
return
}
w.WriteHeader(200)
// fmt.Println("id = " + strconv.Itoa(post.Id)) // 接口类型未定义 Id
return
}
func handlePut(w http.ResponseWriter, r *http.Request, post Text) (err error) {
id, err := strconv.Atoi(path.Base(r.URL.Path)) // 路径中最后一个元素转换成整数
if err != nil {
return
}
// post, err := GetPost(id)
err = post.getPost(id)
if err != nil {
return
}
len := r.ContentLength
body := make([]byte, len)
r.Body.Read(body)
json.Unmarshal(body, &post)
// err = post.Update()
err = post.update()
if err != nil {
return
}
w.WriteHeader(200)
return
}
func handleDelete(w http.ResponseWriter, r *http.Request, post Text) (err error) {
id, err := strconv.Atoi(path.Base(r.URL.Path)) // 路径中最后一个元素转换成整数
if err != nil {
return
}
// post, err := GetPost(id)
err = post.getPost(id)
if err != nil {
return
}
// err = post.Delete()
err = post.delete()
if err != nil {
return
}
w.WriteHeader(200)
return
}
func handleList(t Text) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
// posts, err := Posts(10)
// var posts []Post
posts, err := t.posts(10)
if err != nil {
return
}
output, err := json.MarshalIndent(&posts, "", "\t")
if err != nil {
return
}
w.Header().Set("Content-Type", "application/json")
w.Write(output)
return
}
}
上面的代码中,有一个细节需要指出:data.go中我们改写后的 posts 方法,它的第一个参数改成了 posts []Text,这意味着切片 posts 中的每个元素应当是实现了接口 Text 的类型的实例,而我们在写 posts、getPost、create、update、delete等方法时,方法的接收者是 post *Post,这个意思是实现接口的是 *Post 而非 Post,实现接口的类型是指针类型,从而装入 posts 的必须是指针,这才有了 posts = append(posts, &post) 一句中必须用 &post 取地址形式。如果使用 posts = append(posts, post) ,那么posts、getPost、create、update、delete等方法的接收者必须是 post Post。但 golang 神奇的地方是,如果方法接收者是 post Post,append语句中仍然使用 &post 也是可以的!(这个就是编译器“魔法”,你给出了地址,地址总是指向唯一的东西,如果这个东西就是正确的东西,那么编译器就会施展“魔法”帮你转换好)
接下来,我们来编写同样实现了 Text 接口的 FakePost 类型定义文件 fake.go
package main
type FakePost struct {
Id int
Content string
Author string
}
func (post *FakePost) posts(limit int) (posts []Text, err error) {
return
}
func (post *FakePost) getPost(id int) (err error) {
post.Id = id
return
}
func (post *FakePost) create() (err error) {
return
}
func (post *FakePost) update() (err error) {
return
}
func (post *FakePost) delete() (err error) {
return
}
然后编写测试 server.go 文件的 server_test.go 文件(在之前web服务的测试文件上修改)如下:
package main
import (
"encoding/json"
"net/http"
"net/http/httptest"
"strings"
"testing"
)
func TestHandleGet(t *testing.T) {
mux := http.NewServeMux()
mux.HandleFunc("/post/", handleRequest(&FakePost{})) // 绑定待测 handler
writer := httptest.NewRecorder()
request, _ := http.NewRequest("GET", "/post/1", nil)
mux.ServeHTTP(writer, request) // request请求后,响应的数据 writer 负责
if writer.Code != 200 {
t.Errorf("响应代码 %v", writer.Code)
}
var post FakePost
json.Unmarshal(writer.Body.Bytes(), &post)
if post.Id != 1 {
t.Error("未能获取 JSON post")
}
}
func TestHandlePut(t *testing.T) {
mux := http.NewServeMux()
post := &FakePost{}
mux.HandleFunc("/post/", handleRequest(post)) // 绑定待测 handler
writer := httptest.NewRecorder()
json := strings.NewReader(`{"content":"C++老矣,尚能打否","author":"李四"}`)
request, _ := http.NewRequest("PUT", "/post/1", json)
mux.ServeHTTP(writer, request)
if writer.Code != 200 {
t.Errorf("响应代码 %v", writer.Code)
}
if post.Content != "C++老矣,尚能打否" {
t.Error("内容不正确 ", post.Content)
}
}
值得注意的有2点:1、测试文件 server_test.go 中,必须在每个测试函数中都创建 mux 和 Writer 实例,不然两句 mux.HandleFunc 产生重复注册问题,同时,TestMain 函数和 setUp 函数也不再使用。2、 fake.go 文件其实没有真正测试有关函数,接口函数只是达到了最低返回值要求,保证了测试通过。要真正测试这些函数,会重复执行数据表预设和拆卸操作,过于耗时。
对于 Gocheck 和 Ginkgo 这两个测试库的使用,这里暂时略过,因为学习哪套框架,往往框架有自己的一套约定,针对具体要用的框架学习测试较好。