Go语言测试入门(一)

奇技 · 指南

今天小编为大家分享一篇关于SSH 的介绍和使用方法的文章。本文从SSH是什么出发,讲述了SSH的基本用法,之后在远程登录、端口转发等多种场景下进行独立的讲述,希望能对大家有所帮助。

节省时间和金钱

编写单元测试时,在软件构建阶段会发现许多错误,从而阻止了这些错误过渡到下一个阶段,包括产品发布之后。这节省了在开发生命周期后期修复错误的成本,还为最终用户带来了好处,而最终用户不必处理有问题的产品。此外,你将受益于单元测试所节省出大量时间和资源。

降低循环的复杂性

一个代码块可以有多条路径。条件语句越多,代码块就越复杂。代码越复杂,实现高度的单元测试覆盖率就越困难。除非你进行单元测试,否则你可能不会意识到这种复杂性。有多种方法来衡量复杂度(例如代码覆盖率)。

编写的代码应该符合”单一功能原则“,代码应该只有一个任务。没有单元测试,你的代码就不能说明他已经满足这条原则。实际的工程需要客观且独立的数据来证实观点。

软件交付前已验证且使用

单元测试是锻炼代码以确保代码按照其规范运行的一种方式。如果由于单元测试难以编写而无法交付,那么问题就出在测试之外。所以说单元测试提供了一种更早的发现问题的可能。

文档

没有人真的喜欢写文档,但是单元测试也算是一种文档形式,因为它们表达了在给定上下文中代码应该如何工作。新人接收项目可以先从单元测试看起。

评估修改代码后的

软件需求会随时间变化。所有人都需要对现有功能进行更改。我们还负责估算实现变更所需的工作量。在没有单元测试的情况下,我们只能依靠猜测。这样的猜测是基于直觉和经验的,并不是说如果不进行单元测试,它们就毫无价值。在这种情况下,只能依靠你最有经验的开发人员。

另一方面,如果你具有良好的单元测试覆盖率,你就可以添加更改并运行测试。如果更改导致大量测试失败,那么你就需要确定修改是否正确。首先,你是否正确实施了更改。其次,假设你以唯一的方式实施了更改,那你就需要将其中一部分进行重构,以使你的代码更适合于更改需求。

代码覆盖率

如何能知道将要执行哪一行代码?如果你具有有效的单元测试,则可以快速确定代码是否真正在运行。

在开发环境中,我如何可以快速确定代码是否合格?一个问题是我是否有足够的测试覆盖率。如果我考虑了所有情况,则可以省去代码。如果没有,我至少还要再编写一个测试。如果附加测试需要大量工作,则表明循环复杂性很高。

性能表现

可以使用单元测试来衡量代码的性能。例如,可能需要对散列表进行操作。在现实世界中,此类数据源存在于某些外部数据存储中。为了便于讨论,我们假设已经完成了处理特定操作的代码。随着时间的推移,数据会不停的增加。你不知道增长率。使用单元测试,你可以创建各种方案,从你所知道的,可能的,不可能的。使用单元测试,你可以创建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)
    }
}

编写测试的规则

要编写新的测试,首先需要创建一个名称以_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())
}

运行测试

go test命令为指定的软件包运行测试。

它默认为当前目录中的软件包。

$ go test
PASS
 
$ go test -v
=== RUN TestIndex
--- PASS: TestIndex (0.00 seconds)
PASS

要为所有项目运行测试:

$ go test github.com/nf/...

或测试标准库:

$ go test std

测试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())

往期精彩回顾

kubevirt在360的探索之路(k8s接管虚拟化)

用DNS进行网络度量和安全分析

360Stack裸金属服务器部署实践

360技术公众号

技术干货|一手资讯|精彩活动

扫码关注我们

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值