技术解析
原文出自 山尽写东西的 cache
互联网言必谈代码质量,单元测试,TDD ( Test Driven Development )很久了,仿佛 TDD 就是灵丹妙药,用上了就是质量高没 bug 的代码,但真的是这样子吗?
问题不对,路线错误,测试越多越没用
TDD 很简单,假如你实现整数除法这功能,那么对除法这函数 divide
人工做一些测试用例:
divide(12/1) == 1
divide(12/0) == panic
divide(12/-2) == -6
divide(12/4) == 3
上述例子都通过,那么代码实现就对了。
这当然可以,但人脑就可以穷尽这些特殊例子吗?会有遗漏吗?上面是不是漏了 0/某个数
呢?
是的,上面这些就是特例验证的缺陷 ,你无法证明你就是对的。
那么,我们可以怎么做呢?
本质验证。
整数除法是不是有一些定律特质吗?比如:
0 / a = 0
a / 0 = fatal
a / b / c = a / c / b
a / b * b = a
a / b国外服务器 = 10*a / 10*b
这些就是定律,我们实现的除法在满足这些定律的情况下,就是对的行为。
那么,对 a 与 b 这两个变量,随机生成大量的如一百万个随机数,分别验证上述行为,跑几次没问题后,是不是就证明是对的,且自动化、正确率远远大于 TDD 呢?
没错,这就是Property-based Testing(基于特性测试)
。
以我在工作的例子做示范,我们要实现一个函数,reformat IP 地址的,就是所有的 CIDR IP (如: 192.168.1.0/24:7777"
)或者正常 IP 地址要转换为 IP 地址,去掉没用的/24
,得到192.168.1.0:7777
, 旧代码是这样子做的:
// 10.233.100.175/26:6379 to 10.233.100.175:6379
func ReformatAddress(addr string) string {
slashIndex := strings.IndexByte(addr, '/')
portString := ":6379"
portIndex := strings.IndexByte(addr, ':')
if portIndex >= 0 {
portString = addr[portIndex:]
if slashIndex == -1 {
slashIndex = portIndex
}
return addr[:slashIndex] + portString //只要 / 前的 IP + 端口
}else // 没有端口号
slashIndex = len(addr)
}
return addr[:slashIndex] + portString
}
你可以想想这代码有没有问题。
TDD 的单元测试是这么写的:
func TestReformatAddress(t *testing.T) {
if addr := ReformatAddress("10.233.100.175/1:6379"); addr != "10.233.100.175:6379" { //nolint
t.Errorf("1: %s", addr)
}
if addr := ReformatAddress("10.233.100.175:6379"); addr != "10.233.100.175:6379" { //nolint
t.Errorf("2: %s", addr)
}
if addr := ReformatAddress("10.233.100.175"); addr != "10.233.100.175:6379" { //nolint
t.Errorf("3: %s", addr)
}
}
我用 PBT 重写后就是这样子的:
核心就是ReformatAddress(a)
后的内容,必须是一个正则表达式上个符合 ip 格式的内容
ReformatAddress
,然后用正则表达式校验结果func TestPBTReformatAddress(t *testing.T) {
const ipv4re = `(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])` +
`\.(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])` +
`\.(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])` +
`\.(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])` +
`(/[1-3]?[1-9])?` + // \
`(:^()([1-9]|[1-5]?[0-9]{2,4}|6[1-4][0-9]{3}|65[1-4][0-9]{2}|655[1-2][0-9]|6553[1-5])$)?` // :port range
const validIP4re = `(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])` +
`\.(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])` +
`\.(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])` +
`\.(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])` +
`(:([1-9]|[1-5]?[0-9]{2,4}|6[1-4][0-9]{3}|65[1-4][0-9]{2}|655[1-2][0-9]|6553[1-5])$)` // :port range
rapid.Check(t, func(t *rapid.T) {
addr := rapid.StringMatching(ipv4re).Draw(t, "addr").(string)
fmtAddr := ReformatAddress(addr)
net.ParseIP(strings.Split(fmtAddr, ":")[0])
var re = regexp.MustCompile(validIP4re)
fmt.Printf("origin is %s addr, fmtAddr is %s, match is %v\n", addr, fmtAddr, re.MatchString(fmtAddr))
match := re.MatchString(fmtAddr)
if !match {
t.Fatalf("%s is not correct", fmtAddr)
}
})
}
结果我真就发现了代码有问题,当时修复的截图:
现在代码是这样子的:
// 10.233.100.175/26:6379 to 10.233.100.175:6379
func ReformatAddress(addr string) string {
slashIndex := strings.IndexByte(addr, '/')
portString := ":6379"
portIndex := strings.IndexByte(addr, ':')
if portIndex >= 0 {
portString = addr[portIndex:]
if slashIndex == -1 {
slashIndex = portIndex
}
return addr[:slashIndex] + portString
}
if slashIndex == -1 {
slashIndex = len(addr)
}
return addr[:slashIndex] + portString
}
原因是,不一定所有的入参都一定是对的 CIDR 地址啊,就是不一定 addr 都有/
的。
那这时候slashIndex
是 -1
就有 bug 了,所以要特殊处理。
我工作中还写了很多 PBT test,帮助了好多:
等等等等。
通过这,我几乎 pbt test 对了,几乎就没问题了,堪称完美。
如果这文章对你有启发,请多多点赞转发,感谢