当单元测试撒谎,而Bug依然存在时
您写了20个单元测试。全部通过。CI通过。您很满意。然后——生产环境出现了Bug。熟悉吗?经典场景:您只测试了自己想到的那些情况。而现实偏偏会抛出您甚至没考虑过的组合。
在Go中,有一种强大但被低估的方法——基于属性的测试(或属性测试)。与其检查特定的输入和输出,不如描述函数应该满足的属性,而框架会自动生成数百个随机测试数据。如果属性被违反——您就找到了Bug。
在本文中,我们将探讨如何使用gopter库在Go中实施基于属性的测试,编写实际示例,并看看能捕获哪些Bug。开始吧!
什么是基于属性的测试,为什么需要它?
假设您正在测试一个排序函数。普通的单元测试:
func TestSort(t *testing.T) { input := []int{3, 1, 2} result := sort(input) expected := []int{1, 2, 3} if !reflect.DeepEqual(result, expected) { t.Errorf("got %v, want %v", result, expected) }}好,能工作。但如果数组为空呢?如果所有元素都相同呢?如果有负数呢?您可以再写10个测试,但总会有遗漏。
基于属性的测试从根本上解决了这个问题:您制定不变量——对于任何正确的输入都必须为真的属性。例如,对于排序:
- 排序后数组的长度等于原始数组的长度。
- 原始数组中的每个元素都出现在排序后的数组中(反之亦然)。
- 数组按升序排序。
框架(例如gopter或内置的testing/quick)生成随机输入数据,并检查所有属性是否成立。如果有Bug——即使您不知道在哪里找,也会发现它。
用于基于属性测试的Go库
在Go生态系统中,有几个工具:
testing/quick——内置包,极简,但功能有限。gopter——功能强大的库,带有生成器、缩减(最小化输入数据)和灵活配置。我们推荐使用它。rapid——现代替代方案,性能更高。
我们将使用gopter,因为它有更好的文档和社区。安装:
go get github.com/leanovate/gopter编写第一个基于属性的Go测试
假设我们有一个Reverse函数,用于反转字符串:
func Reverse(s string) string { runes := []rune(s) for i, j := 0, len(runes)-1; i < j; i, j = i+1, j-1 { runes[i], runes[j] = runes[j], runes[i] } return string(runes)}可以检查什么属性?如果反转字符串两次,我们应该得到原始字符串:
import ( "testing" "github.com/leanovate/gopter" "github.com/leanovate/gopter/gen")
func TestReverseTwice(t *testing.T) { properties := gopter.NewProperties(nil)
properties.Property("reverse(reverse(s)) == s", prop.ForAll( func(s string) bool { return Reverse(Reverse(s)) == s }, gen.AnyString(), ))
properties.TestingRun(t)}运行测试——gopter会生成100个随机字符串,检查属性。如果一切正常——测试通过。如果不——您将得到一个反例(导致测试失败的最小字符串)。