你还在用单元测试, TDD?这玩意太不靠谱了!

2021-05-11 16:30:54 +08:00
 ChristopherWu

原文出自 山尽写东西的 cache

互联网言必谈代码质量,单元测试,TDD ( Test Driven Development )很久了,仿佛 TDD 就是灵丹妙药,用上了就是质量高没 bug 的代码,但真的是这样子吗?

单元测试、TDD 的缺陷

问题不对,路线错误,测试越多越没用

TDD 很简单,假如你实现整数除法这功能,那么对除法这函数 divide 人工做一些测试用例:

上述例子都通过,那么代码实现就对了。

这当然可以,但人脑就可以穷尽这些特殊例子吗?会有遗漏吗?上面是不是漏了 0/某个数呢?

是的,上面这些就是特例验证的缺陷 ,你无法证明你就是对的。

Property-based Testing

那么,我们可以怎么做呢?

本质验证。

整数除法是不是有一些定律特质吗?比如:

这些就是定律,我们实现的除法在满足这些定律的情况下,就是对的行为。

那么,对 a 与 b 这两个变量,随机生成大量的如一百万个随机数,分别验证上述行为,跑几次没问题后,是不是就证明是对的,且自动化、正确率远远大于 TDD 呢?

没错,这就是Property-based Testing(基于特性测试)

golang 的 PBT test

以我在工作的例子做示范,我们要实现一个函数,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 格式的内容

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,帮助了好多:

等等等等。

通过这,我几乎 pbt test 对了,几乎就没问题了,堪称完美。

如果这文章对你有启发,请多多点赞转发,感谢

1886 次点击
所在节点    程序员
4 条回复
ChristopherWu
2021-05-11 16:50:50 +08:00
看来我是降维了..
he1a2s0
2021-05-17 14:46:08 +08:00
这个理论上也属于单元测试吧,我只知道.net 的 xunit 测试里面这个叫 Theory
ChristopherWu
2021-05-17 18:54:33 +08:00
@he1a2s0 对,其实用在单元测试上跟集成测试上都可以。这个实际上可以叫 generative test
Zzhiter
204 天前
老哥牛啊,最近我也在看这个,感觉可以抽象出来一些基本操作的对应的可以验证的属性。

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/776279

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX