你还在用单元测试, TDD?这玩意太不靠谱了! - V2EX
V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
请不要在回答技术问题时复制粘贴 AI 生成的内容
ChristopherWu
V2EX    程序员

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

  •  1
    nbsp;
  •   ChristopherWu 2021-05-11 16:30:54 +08:00 2310 次点击
    这是一个创建于 1617 天前的主题,其中的信息可能已经有所发展或是发生改变。

    原文出自 山尽写东西的 cache

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

    单元测试、TDD 的缺陷

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

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

    • 除 1 divide(12/1) == 1
    • 除 0 divide(12/0) == panic
    • 除某个负数 divide(12/-2) == -6
    • 除某个正数 divide(12/4) == 3

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

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

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

    Property-based Testing

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

    本质验证。

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

    • 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(基于特性测试)

    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 格式的内容

    • 根据正则表达式生成合理的 CIDR 地址或者 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,帮助了好多:

    • 某服务主从切换
      • 多次随机启动停止事件,都要满足有一个主,其他都是它的 slave 的特质
    • 某服务 HA 高可用
      • 多次 n 个服务随机停止启动事件,至少保证有一个在接收处理请求

    等等等等。

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

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

    4 条回复    2024-05-08 15:12:36 +08:00
    ChristopherWu
        1
    ChristopherWu  
    OP
       2021-05-11 16:50:50 +08:00
    看来我是降维了..
    he1a2s0
        2
    he1a2s0  
       2021-05-17 14:46:08 +08:00
    这个理论上也属于单元测试吧,我只知道.net 的 xunit 测试里面这个叫 Theory
    ChristopherWu
        3
    ChristopherWu  
    OP
       2021-05-17 18:54:33 +08:00
    @he1a2s0 对,其实用在单元测试上跟集成测试上都可以。这个实际上可以叫 generative test
    Zzhiter
        4
    Zzhiter  
       2024-05-08 15:12:36 +08:00
    老哥牛啊,最近我也在看这个,感觉可以抽象出来一些基本操作的对应的可以验证的属性。
    关于     帮助文档     自助推广系统     博客     API     FAQ     Solana     1071 人在线   最高记录 6679       Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 23ms UTC 18:13 PVG 02:13 LAX 11:13 JFK 14:13
    Do have faith in what you're doing.
    ubao snddm index pchome yahoo rakuten mypaper meadowduck bidyahoo youbao zxmzxm asda bnvcg cvbfg dfscv mmhjk xxddc yybgb zznbn ccubao uaitu acv GXCV ET GDG YH FG BCVB FJFH CBRE CBC GDG ET54 WRWR RWER WREW WRWER RWER SDG EW SF DSFSF fbbs ubao fhd dfg ewr dg df ewwr ewwr et ruyut utut dfg fgd gdfgt etg dfgt dfgd ert4 gd fgg wr 235 wer3 we vsdf sdf gdf ert xcv sdf rwer hfd dfg cvb rwf afb dfh jgh bmn lgh rty gfds cxv xcv xcs vdas fdf fgd cv sdf tert sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf shasha9178 shasha9178 shasha9178 shasha9178 shasha9178 liflif2 liflif2 liflif2 liflif2 liflif2 liblib3 liblib3 liblib3 liblib3 liblib3 zhazha444 zhazha444 zhazha444 zhazha444 zhazha444 dende5 dende denden denden2 denden21 fenfen9 fenf619 fen619 fenfe9 fe619 sdf sdf sdf sdf sdf zhazh90 zhazh0 zhaa50 zha90 zh590 zho zhoz zhozh zhozho zhozho2 lislis lls95 lili95 lils5 liss9 sdf0ty987 sdft876 sdft9876 sdf09876 sd0t9876 sdf0ty98 sdf0976 sdf0ty986 sdf0ty96 sdf0t76 sdf0876 df0ty98 sf0t876 sd0ty76 sdy76 sdf76 sdf0t76 sdf0ty9 sdf0ty98 sdf0ty987 sdf0ty98 sdf6676 sdf876 sd876 sd876 sdf6 sdf6 sdf9876 sdf0t sdf06 sdf0ty9776 sdf0ty9776 sdf0ty76 sdf8876 sdf0t sd6 sdf06 s688876 sd688 sdf86