一、为什么要编写测试?
我的观点是:单元测试可能看起来很繁琐,但是从长远来看,它的好处是显而易见的。单元测试可以确保所有代码在部署之前均符合质量标准。在产品开发生命周期的整个过程中,单元测试可以节省时间和金钱,并帮助开发人员更有效地编写更好的代码。
1.节省时间和金钱
编写单元测试时,在软件构建阶段会发现许多错误,从而阻止了这些错误过渡到下一个阶段,包括产品发布之后。这节省了在开发生命周期后期修复错误的成本,还为最终用户带来了好处,而最终用户不必处理有问题的产品。此外,你将受益于单元测试所节省出大量时间和资源。
2.降低循环的复杂性
一个代码块可以有多条路径。条件语句越多,代码块就越复杂。代码越复杂,实现高度的单元测试覆盖率就越困难。除非你进行单元测试,否则你可能不会意识到这种复杂性。有多种方法来衡量复杂度(例如代码覆盖率)。
编写的代码应该符合”单一功能原则“,代码应该只有一个任务。没有单元测试,你的代码就不能说明他已经满足这条原则。实际的工程需要客观且独立的数据来证实观点。
3.软件在交付前已验证且使用
单元测试是锻炼代码以确保代码按照其规范运行的一种方式。如果由于单元测试难以编写而无法交付,那么问题就出在测试之外。
所以说单元测试提供了一种更早的发现问题的可能。
4.文档
没有人真的喜欢写文档,但是单元测试也算是一种文档形式,因为它们表达了在给定上下文中代码应该如何工作。
新人接收项目可以先从单元测试看起。
5.评估修改代码后的正确性
软件需求会随时间变化。所有人都需要对现有功能进行更改。我们还负责估算实现变更所需的工作量。在没有单元测试的情况下,我们只能依靠猜测。这样的猜测是基于直觉和经验的,并不是说如果不进行单元测试,它们就毫无价值。在这种情况下,只能依靠你最有经验的开发人员。
另一方面,如果你具有良好的单元测试覆盖率,你就可以添加更改并运行测试。如果更改导致大量测试失败,那么你就需要确定修改是否正确。首先,你是否正确实施了更改。其次,假设你以唯一的方式实施了更改,那你就需要将其中一部分进行重构,以使你的代码更适合于更改需求。
6.代码覆盖率
如何能知道将要执行哪一行代码?如果你具有有效的单元测试,则可以快速确定代码是否真正在运行。
在开发环境中,我如何可以快速确定代码是否合格?一个问题是我是否有足够的测试覆盖率。如果我考虑了所有情况,则可以省去代码。如果没有,我至少还要再编写一个测试。如果附加测试需要大量工作,则表明循环复杂性很高。
7.性能表现
可以使用单元测试来衡量代码的性能。例如,可能需要对散列表进行操作。在现实世界中,此类数据源存在于某些外部数据存储中。为了便于讨论,我们假设已经完成了处理特定操作的代码。随着时间的推移,数据会不停的增加。你不知道增长率。使用单元测试,你可以创建各种方案,从你所知道的,可能的,不可能的。使用单元测试,你可以创建10万项哈希,看看会发生什么。单元测试提供了评估性能的能力。
二、如何编写go的测试?
Go自有轻量级的测试框架,它由go test 命令和testing包组成。
这是测试strings.Index函数的完整测试文件:
package strings_test
import (
"strings"
"testing"
)
func TestIndex(t *testing.T) {
const s, sep, want = "chicken", "ken", 4
got := strings.Index(s, sep)
if got != want {
t.Errorf("Index(%q,%q) = %v; want %v", s, sep, got, want)
}
}
1.编写测试的规则
要编写新的测试,首先需要创建一个名称以_test.go结尾的文件,该文件需要包含TestXxx函数,其中Xxx不以小写字母开头。
将该测试文件放在要测试文件的同一个包中,go会将该文件从常规软件包生成时排除,只有在运行 go test命令时才会包括在内。
project
|-main.go
|-main_test.go
| -api
| |-route.go
| |-route_test.go
| |-handlers.go
| `-handlers_test.go
|-storage.go`
|-storage_test.go
关于命名约定:
func Example() { ... } // package
func ExampleF() { ... } //function
func ExampleT() { ... } //type
func ExampleT_M() { ... } // method
可以通过名称后附加一个不同的后缀来提供多个示例,后缀必须以小写字母开头。
func Example_suffix() { ... }
func ExampleF_suffix() { ... }
func ExampleT_suffix() { ... }
func ExampleT_M_suffix() { ... }
type T
type T struct {
// contains filtered or unexported fields
}
T 是传递给测试函数的一种类型,它用于管理测试状态并支持格式化测试日志。测试日志会在执行测试的过程中不断累积, 并在测试完成时输出至标准输出。
当一个测试的测试函数返回时, 又或者当一个测试函数调用 FailNow 、 Fatal 、 Fatalf 、 SkipNow 、 Skip 或者 Skipf 中的任意一个时, 该测试即宣告结束。 跟 Parallel 方法一样, 以上提到的这些方法只能在运行测试函数的 goroutine 中调用。
至于其他报告方法, 比如 Log 以及 Error 的变种, 则可以在多个 goroutine 中同时进行调用。
* testing.T参数用于错误报告:
如果函数调用了一个失败函数,比如t.Error或t.Fail,则认为测试失败。
t.Errorf("got bar = %v, want %v", got, want)
t.Fatalf("Frobnicate(%v) returned error: %v", arg, err)
t.Logf("iteration %v", i)
// 获取测试名称
method (*T) Name() string
// 打印日志
method (*T) Log(args ...interface{})
// 打印日志,支持 Printf 格式化打印
method (*T) Logf(format string, args ...interface{})
// 反馈测试失败,但不退出测试,继续执行
method (*T) Fail()
// 反馈测试成功,立刻退出测试
method (*T) FailNow()
// 反馈测试失败,打印错误
method (*T) Error(args ...interface{})
// 反馈测试失败,打印错误,支持 Printf 的格式化规则
method (*T) Errorf(format string, args ...interface{})
// 检测是否已经发生过错误
method (*T) Failed() bool
// 相当于 Error + FailNow,表示这是非常严重的错误,打印信息结束需立刻退出。
method (*T) Fatal(args ...interface{})
// 相当于 Errorf + FailNow,与 Fatal 类似,区别在于支持 Printf 格式化打印信息;
method (*T) Fatalf(format string, args ...interface{})
// 跳出测试,从调用 SkipNow 退出,如果之前有错误依然提示测试报错
method (*T) SkipNow()
// 相当于 Log 和 SkipNow 的组合
method (*T) Skip(args ...interface{})
// 与Skip,相当于 Logf 和 SkipNow 的组合,区别在于支持 Printf 格式化打印
method (*T) Skipf(format string, args ...interface{})
// 用于标记调用函数为 helper 函数,打印文件信息或日志,不会追溯该函数。
method (*T) Helper()
// 标记测试函数可并行执行,这个并行执行仅仅指的是与其他测试函数并行,相同测试不会并行。
method (*T) Parallel()
// 可用于执行子测试
method (*T) Run(name string, f func(t *T)) bool
type B
type B struct {
N int
// contains filtered or unexported fields
}
B 是传递给基准测试函数的一种类型,它用于管理基准测试的计时行为,并指示应该迭代地运行测试多少次。
一个基准测试在它的基准测试函数返回时,又或者在它的基准测试函数调用 FailNow、Fatal、Fatalf、SkipNow、Skip 或者 Skipf 中的任意一个方法时,测试即宣告结束。至于其他报告方法,比如 Log 和 Error 的变种,则可以在其他 goroutine 中同时进行调用。
跟单元测试一样,基准测试会在执行的过程中记录日志,并在测试完毕时将日志输出到标准错误。但跟单元测试不一样的是,为了避免基准测试的结果受到日志打印操作的影响,基准测试总是会把日志打印出来。
type M
type M struct {
// contains filtered or unexported fields
}
M 是传递给 TestMain 函数以运行实际测试的类型。即测试需要在主函数上运行代码时使用:
func TestMain(m *testing.M)
生成的测试将调用 TestMain(m),而不是直接运行测试。
TestMain 运行在主 goroutine 中, 可以在调用 m.Run 前后做任何设置和拆卸。应该使用 m.Run 的返回值作为参数调用 os.Exit。在调用 TestMain 时, flag.Parse 并没有被调用。所以,如果 TestMain 依赖于 command-line 标志 (包括 testing 包的标记), 则应该显示的调用 flag.Parse。
一个简单的 TestMain 的实现:
func TestMain(m *testing.M) {
// 如果 TestMain 使用了 flags,这里应该加上 flag.Parse()
os.Exit(m.Run())
}
2.运行测试
go test命令为指定的软件包运行测试。
它默认为当前目录中的软件包。
$ go test
PASS
$ go test -v
=== RUN TestIndex
--- PASS: TestIndex (0.00 seconds)
PASS
要为所有项目运行测试:
$ go test github.com/nf/...
或测试标准库:
$ go test std
3.测试HTTP客户端和服务器
net/http/httptest 包提供了测试发出HTTP请求和处理HTTP请求的代码。
httptest.Server
httptest.Server侦听本地回送接口上系统选择的端口,以用于端到端HTTP测试。
type Server struct {
// 一个末尾不包含斜线、格式为 http://ipaddr:port 的基本 URL
Listener net.Listener
// 可选的 TLS 配置选项(configuration),它将在 TLS 启动之后被设置成一个新的配置(config)。
// 如果用户在调用 StartTLS 之前,对一个尚未启动的服务器的 TLS 配置进行了设置,
// 那么已设置的字段将被拷贝至新配置里面。
TLS *tls.Config
// Config 的值有可能在调用 NewUnstartedServer 之后或者调用
// Start 和 StartTLS 之前发生变化。
Config *http.Server
}
func NewServer(handler http.Handler) *Server
func (*Server) Close() error
这段代码设置了一个临时HTTP服务器,该服务器提供简单的“ Hello”响应。
httptest.Server 示例代码
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, "Hello, client")
}))
defer ts.Close()
res, err := http.Get(ts.URL)
if err != nil {
log.Fatal(err)
}
greeting, err := ioutil.ReadAll(res.Body)
res.Body.Close()
if err != nil {
log.Fatal(err)
}
fmt.Printf("%s", greeting)
httptest.ResponseRecorder
httptest.ResponseRecorder是http.ResponseWriter的实现,它会记录自身的变化以便在之后的测试中进行检验。
type ResponseRecorder struct {
// Code 是由 WriteHeader 设置的 HTTP 响应码。
//
// 注意,如果一个处理器从来没有调用过 WriteHeader 或者 Write ,
// 那么 Code 的值将会为 0 ,而不是隐含的 http.StatusOK 。
// 如果用户想要在 Code 值为 0 时获得隐含的 http.StatusOK ,
// 那么可以使用 Result 方法。
Code int
// HeaderMap 包含了处理器显式设置的各个 HTTP 首部。
//
// 如果用户想要获得诸如 Content-Type 等一系列由服务器隐式地进行设置的首部,
// 那么可以使用 Result 方法。
HeaderMap http.Header
// Body 是处理器调用 Write 进行写入时所使用的缓冲区。
// 如果它的值为 nil ,那么写入的数据将会被静默地丢弃(silently discarded)。
Body *bytes.Buffer
// Flushed 标记处理器是否调用了 Flush 方法。
Flushed bool
}
httptest.ResponseRecorder 示例代码
通过将ResponseRecorder传递到HTTP处理程序中,我们可以检查生成的响应。
handler := func(w http.ResponseWriter, r *http.Request) {
http.Error(w, "something failed", http.StatusInternalServerError)
}
req, err := http.NewRequest("GET", "http://example.com/foo", nil)
if err != nil {
log.Fatal(err)
}
w := httptest.NewRecorder()
handler(w, req)
fmt.Printf("%d - %s", w.Code, w.Body.String())