Go语言实战-9章 测试和性能
一些单元测试可能会测试负向路径的场景,保证代码不仅会产生错误,而且是预期的错误。这种场景下的测试可能是对数据库进行查询时没有找到任何结果,或者对数据库做了无效的更新。在这两种情况下,测试都要验证确实产生了错误,且产生的是预期的错误。总之,不管如何调用或者执行代码,所写的代码行为都是可预期的。在 Go 语言里有几种方法写单元测试。基础测试(basic test)只使用一组参数和结果来测试一段代码。表组测试(table test)也会测试一段代码,但是会使用多组参数和结果进行测试。也可以使用一些方法来模仿(mock)测试代码需要使用到的外部资源,如数据库或者网络服务器。这有助于让测试在没有所需的外部资源可用的时候,模拟这些资源的行为使测试正常进行。
不但函数名字要以 Test 开头,而且函数的签名必须接收一个指向 testing.T 类型的指针,并且不返回任何值。如果没有遵守这些约定,测试框架就不会认为这个函数是一个测试函数,也不会让测试工具去执行它。
如果执行 go test 的时候没有加入冗余选项(-v),除非测试失败,否则我们是看不到任何测试输出的。
t.Fatal 方法不但报告这个单元测试已经失败,而且会向测试输出写一些消息,而后立刻停止这个测试函数的执行。如果除了这个函数外还有其他没有执行的测试函数,会继续执行其他测试函数。这个方法对应的格式化版本名为 t.Fatalf。
如果测试函数执行时没有调用过 t.Fatal或者 t.Error 方法,就会认为测试通过了。
依赖不属于你的或者你无法操作的服务来进行测试,也不是一个好习惯。这两点会严重影响测试持续集成和部署的自动化。如果突然断网,导致测试失败,就没办法部署新构建的程序。
包 httptest 可以让你能够模仿互联网资源的请求和响应。在我们的单元测试中,通过模仿 http.Get 的响应,我们可以解决在图 9-4 中遇到的问题,保证在没有网络的时候,我们的测试也不会失败,依旧可以验证我们的 http.Get 调用正常工作,并且可以处理预期的响应。
服务端点(endpoint)是指与服务宿主信息无关,用来分辨某个服务的地址,一般是不包含宿主的一个路径。如果在构造网络 API,你会希望直接测试自己的服务的所有服务端点,而不用启动整个网络服务。包 httptest 正好提供了做到这一点的机制。让我们看一个简单的包含一个服务端点的网络服务的例子
如果包使用这种方式命名,测试代码只能访问包里公开的标识符。即便测试代码文件和被测试的代码放在同一个文件夹中,也只能访问公开的标识符。就像直接运行服务时一样,需要为服务端点初始化路由,
如果包使用这种方式命名,测试代码只能访问包里公开的标识符。即便测试代码文件和被测试的代码放在同一个文件夹中,也只能访问公开的标识符。就像直接运行服务时一样,需要为服务端点初始化路由,
对于示例代码,需要遵守一个规则。示例代码的函数名字必须基于已经存在的公开的函数或者方法。我们的示例的名字基于 handlers 包里公开的 SendJSON 函数。如果没有使用已经存在的函数或者方法,这个示例就不会显示在包的 Go 文档里。
基准测试是一种测试代码性能的方法。想要测试解决同一问题的不同方案的性能,以及查看哪种解决方案的性能更好时,基准测试就会很有用。基准测试也可以用来识别某段代码的 CPU或者内存效率问题,而这段代码的效率可能会严重影响整个应用程序的性能。许多开发人员会用基准测试来测试不同的并发模式,或者用基准测试来辅助配置工作池的数量,以保证能最大化系统的吞吐量。
testing.T. 有一个方法叫 ResetTimer()。基准测试里面调用 b.ResetTimer 的作用。在代码开始执行循环之前需要进行初始化时,这个方法用来重置计时器,保证测试代码执行前的初始化代码,不会干扰计时器的结果。为了保证得到的测试结果尽量精确,需要使用这个函数来跳过初始化代码的执行时间。
基准测试函数必须以 Benchmark 开头,接受一个指向 testing.B 类型的指针作为唯一参数。为了让基准测试框架能准确测试性能,它必须在一段时间内反复运行这段代码,
基准测试框架默认会在持续 1 秒的时间内,反复调用需要测试的函数。测试框架每次调用测试函数时,都会增加 b.N 的值。第一次调用时,b.N 的值为 1。需要注意,一定要将所有要进行基准测试的代码都放到循环里,并且循环要使用 b.N 的值。否则,测试的结果是不可靠的。
默认情况下,基准测试的最小运行时间是 1 秒。如果想让运行时间更长,可以使用另一个名为-benchtime 的
选项来更改测试执行的最短时间。对大多数测试来说,超过 3 秒的基准测试并不会改变测试的精确度。
运行基准测试时,另一个很有用的选项是-benchmem 选项。这个选项可以提供每次操作分配内存的次数,以及总共分配内存的字节数。输出的结果会多出两组新的数值:一组数值的单位是 B/op,另一组的单位是
allocs/op。单位为 allocs/op 的值表示每次操作从堆上分配内存的次数。你可以看到Sprintf 函数每次操作都会从堆上分配两个值,而另外两个函数每次操作只会分配一个值。单位为 B/op 的值表示每次操作分配的字节数。你可以看到 Sprintf 函数两次分配总共消耗了 16字节的内存,而另外两个函数每次操作只会分配 2 字节的内存。