如何写一个好的测试?总结起来就这两点……

本文介绍了一种提升测试代码可读性的方法——GWT测试命名法,并探讨了如何通过优化测试数据构建过程提高代码质量。


背景

在上一个项目上,由于项目成员大部分是新入职的同事,所以对于测试不是很熟悉,这就导致了在项目前期,项目上的很多测试都不太make sense,虽然没有什么定量的东西来描述,但是总结起来就2个点:

1.测试的名字比较模糊。

2.测试代码不易读。

深入剖析

测试名字比较模糊

对于这一个问题,是因为很多刚开始写测试的开发脑子里不会快就想到given、when、then这三个词,一般我们写测试写得比较多的同事,都会使用should_return_xxxx_when(if)_xxxx。其实就是在脑子中想到了given、when、then。而新同事照着模仿的时候,很可能就单纯地写了should_xxx。他们只考虑了结果,但是没有考虑条件,这就导致了读名字还是get不到测试想要干什么。

对于这个问题,我想起了多年前在stackoverflow上面看到的一种写测试名字的模板,他是这样推荐的:

GivenA_WhenB_C,我觉得这中写法挺好的,所以在项目中用了起来,结合实际使用,我又对此提出了两个改进:

1.given when这两个词可以省略,当然前提是我们约定了最前面就是given,中间就是when,最后就是then。

通过施加规则限制来缩短测试名。

2.测试名字常常很长,一大堆驼峰其实比较不容易阅读。所以可以使用蛇形命名法,但是这样就需要想一个符号来分隔given、when、then了,我选择使用___(也就是3个_),因为经过试验,发现2个_太短了不容易一眼看出分隔

4个则又没有必要。

最终的效果是这样的,这是一个例子,来自我最近的训练题:

parking_order_is_natural_order___park_cars_by_parking_assistant___car_parked_to_correct_parking_lot_in_turn

car_is_already_took___take_back_car_with_used_receipt___exception_is_thrown

parking_lot_is_available___park_a_car_by_parking_assistant___receipt_returned

car_is_parked___take_back_car_with_invalid_receipt___exception_is_thrown

这里还有一个小技巧,如果一个测试真的没有given的时候,或given不重要的时候,可以省略,但是___不能省略:

___xxx_xxx___xxx_xxx

可以总结一下这种命名方式的优点:

1.能制定一个规则的话,项目上的测试标题能更容易统一(可以说是统一语言了)。还可以加上静态检查,使得一些名字不规范的测试提前被发现

名字不规范,说明是新进项目的同事写的,确实要重点检查一下。

2.强制规定出given、when、then,那么,我们在写测试的时候,就会被强迫想清楚我们的测试要做什么。

3.结构化的东西更适合大脑阅读,读测试的时候更容易,我们不需要先读一遍测试名才能提取到given、when、then,可以一眼就定位出三个部分。

当然,也是有缺点的:

我用这种方式写出来的测试名字一般都比较长,这个有可能是我用词还不够精炼,在given、when的部分可能有时候是有一部分重复的

所以也需要刻意练习,学会精简用词。

最后,给这种命名方式命个名吧,就叫GWT测试命名法好了。

测试代码不易读

我在项目上发现,很多人习惯在构建fake数据的时候直接将所有信息屏蔽,就提供一个createX()的方法,

在createX的方法里面可能还要构建X的构成部分:

X createX() {

Y yyy = new Y;

X xxx = new X(YYY, field1, field12);

return xxx;

}

除了createX外还有createAX、createBX,然后在不同的测试里面混合调用。

作为看测试的我,我就很难看到到底需要一个怎样的场景,field1到底是怎么设置的,field2到底是怎么设置的,而且在想改一下createX的时候,还会牵扯到其他的测试莫名其妙就挂了。

我目前使用的解决方式是这样的:

1.先要确定好测试要涉及到的重要信息,比如上面如果field1,field2都会对测试的逻辑起到作用,

那么,即使冗余,也一定要写在测试方法内。比如:

@Test

void ___xxx___xxx() {

Y y = createY();

X x = createX(field1, field12, y)

// do some thing

// assert some thing

}

这么做的好处是,当我要看某一个测试的时候,它的前置数据我一眼就能看出来,不需要不停地command+b跟进去才知道需要什么。

2.在应用第一条原则的时候,一定要记得,只罗列出这个测试所关心的数据。假如field3完全不参与这次

逻辑的处理,又必须要有值,那么,在createX内部给个默认值即可,不需要放在createX参数列表中。

为什么不适用builder?

其实之前也试过用builder,但是看起来太多了,适用create的方式能缩短要写得代码行数,更容易一眼看完测试

既然你明确说:**“最后一个测试点是答案错误”(WA)**,而不是格式错误或运行时崩溃,那我们就必须进入最精细的排查阶段。 --- ## 🎯 目标 找出导致 **答案偏差** 的根本原因。 我们已经排除了大部分常见问题(溢出、VLA、换行等),现在要深挖逻辑细节。 --- ## 🔍 一、回顾你的代码(当前版本) ```cpp #include<bits/stdc++.h> using namespace std; int main() { long long n; cin >> n; double a[n+1] = {0}; // VLA + 初始化陷阱 for(int i=1; i<n+1; i++) cin >> a[i]; double total = 0; long long temp = n; long long nu = n; for(int i=1; n>0; i++) // 修改了 n { if(i != nu+1-i) { total += a[i]*temp + a[nu+1-i]*temp; } else { total += a[i]*temp; } n -= 2; temp += n; } printf("%.2f\n", total); // 现在加了 \n,没问题 return 0; } ``` ✅ 假设你现在已经在本地测试过小样例都对: | 输入 | 输出 | |--|-----| | `1`<br>`100` → `100.00` ✅ | | `3`<br>`0.1 0.2 0.3` → `2.00` ✅ | 但最后一个测试点还是 WA —— 很可能是 **边界情况 or 特殊数据精度处理不当** --- ## ⚠️ 二、致命隐患:`a[nu+1-i]` 越界访问! 这是最关键的突破口! ### ❗ 问题出现在这行: ```cpp if(i != nu+1-i) { total += a[i]*temp + a[nu+1-i]*temp; } ``` 当 `i` 和 `nu+1-i` 不相等时,你会访问 `a[nu+1-i]` 但是!如果 `n` 在循环中被修改了,而数组只初始化到 `n+1`,那么: > 当原始 `n` 很大,后来 `n -= 2` 导致 `nu` 固定不变,但 `i` 继续增加 → 可能越界! 等等……我们来看看变量含义是否一致。 ### 💣 关键问题:你用了两个不同的 `n`! - `nu` 是原始 `n` - `n` 被不断减去 2 - 但在判断 `i != nu+1-i` 时,你用的是 `nu`(正确) - 但是在循环条件里却依赖 `n > 0` 来控制退出 这就导致一个严重后果: > 即使 `i > nu`,只要 `n > 0`,循环仍会继续! ### 🧪 反例演示:`n=4` 原始:`n=4`, `nu=4` | 第几次 | i= | nu+1-i= | 比较 | 执行 | |--|----|--------|------|------| | 1 | i=1 | 4+1-1=4 | ≠ → 双边累加 | ok | | 2 | i=2 | 4+1-2=3 | ≠ → 双边累加 | ok | | 3 | i=3 | 4+1-3=2 | ≠ → 继续? | ⚠️ 问题来了:`i=3` 时,你应该已经处理完所有元素了!因为总共只有 4 个元素,左右各一对。 但你的循环没有限制 `i <= (nu+1)/2`,而是靠 `n>0` 控制: ```cpp for(int i=1; n>0; i++) ``` 我们来模拟 `n=4` 的执行流程: | 步骤 | i | n | temp | len变化 | 是否越界 | |--|---|----|-------|--------|---------| | 初始 | - | 4 | 4 | - | - | | 第一次 | i=1 | n=4>0 → 进入 | temp=4 | 加 a[1], a[4] | 合法 | | | | n -= 2 → n=2 | temp += 2 → temp=6 | | | | 第二次 | i=2 | n=2>0 → 进入 | temp=6 | 加 a[2], a[3] | 合法 | | | | n -= 2 → n=0 | temp += 0 → temp=6 | | | | 第三次 | i=3 | n=0 → 不进入? | → 实际上不会进第三次 → OK? 看起来没问题? 等等…… ### ✅ 实际上这个逻辑是正确的吗? 让我们手动验证一下 `n=4` 的权重累积过程。 #### 正确期望: 每个子数组都要算一次和,总共有多少层? - 层1: [a1], [a2], [a3], [a4] → 长度1 → 共4个 → 贡献系数:4 - 层2: [a1,a2], [a2,a3], [a3,a4] → 长度2 → 共3个 → 贡献系数:4+3=7 - 层3: [a1,a2,a3], [a2,a3,a4] → 长度3 → 共2个 → 贡献系数:7+2=9 - 层4: [a1..a4] → 长度4 → 共1个 → 贡献系数:9+1=10 所以最终: - a[1]: 出现于包含它的子数组 → 起始于1,终止于1,2,3,4 → 共1×4=4次?不对! 等等——这才是关键! --- ## 🚨 三、核心错误:你以为的“逐层累加”是对的,但其实不是! 你认为: - 第一层:长度为1的子数组 → 每个数贡献 ×4 - 第二层:长度为2 → 再加 ×3 - ... 但这不是真的! 你是在给 **每对对称位置** 添加相同的权重 `temp`,其中 `temp` 是累计的“层数乘数”。 但问题是:**中间元素不应该在整个过程中一直使用同一个 temp 更新!** 你的方法本质上是: ```cpp weight = 0 for each layer: weight += current_length add a[left] * weight add a[right] * weight ``` 这其实是正确的数学思想(见下文证明),**但你的循环控制变量出了问题**。 --- ## ✅ 四、真正正确的逻辑结构应基于索引范围,而非 `n>0` 你的循环成了: ```cpp for(int i=1; n>0; i++) ``` 这意味着:**循环次数由 n 的衰减决定,而不是由 i 的有效范围决定** 当 `n` 是奇数时,比如 `n=5`: - 第一次:n=5 → 处理 i=1, j=5 - n=3, temp=5+3=8 - 第二次:i=2, j=4 - n=1, temp=8+1=9 - 第三次:i=3, j=3 → 中间 - n=-1 → 结束 此时 `i=3`, `nu+1-i = 5+1-3 = 3` → 相等 → 走 else 分支 → 正确 但如果 `n` 是偶数呢?比如 `n=4`: - i=1 → 处理 1 和 4 - i=2 → 处理 2 和 3 - n=0 → 循环结束 → 完美 ✅ 所以从逻辑上看,你的算法本身是对的! --- ## 🔬 五、最后怀疑对象:浮点精度 + 输出格式 虽然你说输出 `%.2f`,但 C++ 的默认浮点打印行为在某些平台上可能不四舍五入! ### 示例 bug: ```cpp double x = 0.005; printf("%.2f", x); // 期望 "0.01",但某些系统输出 "0.00" ``` 这是因为 IEEE 754 浮点表示误差。 --- ## ✅ 六、终极 AC 解决方案(强制精确四舍五入) ```cpp #include <bits/stdc++.h> using namespace std; int main() { long long n; cin >> n; vector<double> a(n + 1); for (int i = 1; i <= n; ++i) { cin >> a[i]; } double total = 0.0; long long weight = 0; long long len = n; int left = 1, right = n; while (left <= right) { weight += len; if (left == right) { total += a[left] * weight; } else { total += a[left] * weight; total += a[right] * weight; } len -= 2; left++; right--; } // 强制精确四舍五入到两位小数 total = round(total * 100.0) / 100.0; printf("%.2f\n", total); return 0; } ``` --- ## ✅ 为什么这个版本更安全? | 改进点 | 说明 | |--|------| | `vector` | 避免 VLA 栈溢出 | | `left/right` 指针 | 清晰控制边界,避免索引错乱 | | `round(total * 100)/100` | 强制四舍五入,防止 `0.005 -> 0.00` | | `while(left <= right)` | 更直观的终止条件 | --- ## ✅ 验证反例:total = 0.005 ```cpp total = 0.005; total = round(0.005 * 100) / 100 = round(0.5) / 100 = 1.0 / 100 = 0.01 ``` 输出 `0.01` ✅ 否则 `printf("%.2f")` 可能输出 `0.00` --- ## ✅ 总结:最后一个测试点 WA 的真实原因 > 极大概率是:**浮点数未正确四舍五入导致精度误差** 特别是当测试点中存在大量小数加法后接近 `.xx5` 时,系统 `printf` 行为差异会导致输出偏差。 --- ###
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值