WEBVTT

1
00:00:00.000 --> 00:00:04.985
仪表盘的等级字段有一份范围很窄的约定：它只接受一个数字，

2
00:00:04.985 --> 00:00:07.477
0 到 9，并返回对应的值。

3
00:00:07.477 --> 00:00:11.749
其他任何字符串都必须抛出 ValueError。

4
00:00:12.083 --> 00:00:14.361
这是一个候选的解析函数。

5
00:00:14.361 --> 00:00:19.107
输入 3，它返回 3，现有的正常路径测试也通过了。

6
00:00:19.450 --> 00:00:20.456
所以测试是绿的。

7
00:00:20.456 --> 00:00:23.978
可是，如果代码里真有一个实质性的缺陷，这些测试能发现吗？

8
00:00:25.016 --> 00:00:27.376
从约定出发，而不是从代码出发。

9
00:00:27.376 --> 00:00:30.364
先写下期望的结果：输入 3 得到 3；

10
00:00:30.364 --> 00:00:35.082
空字符串和 12 都必须被拒绝，抛出 ValueError。

11
00:00:35.416 --> 00:00:37.160
现在追踪一下候选实现。

12
00:00:37.160 --> 00:00:40.806
对 12，它只读第一个字符，悄悄地返回了 1。

13
00:00:40.806 --> 00:00:45.562
对空字符串，取下标直接崩溃，抛出的是 IndexError，

14
00:00:45.562 --> 00:00:48.733
而不是约定要求的 ValueError。

15
00:00:49.066 --> 00:00:50.852
这是两个不同的缺陷。

16
00:00:50.852 --> 00:00:53.710
只检查合法输入，两个都发现不了。

17
00:00:54.749 --> 00:00:56.993
写测试之前还有一个陷阱。

18
00:00:56.993 --> 00:01:00.731
假设期望值是从候选实现的返回值里抄来的。

19
00:01:00.731 --> 00:01:05.779
它对 12 返回 1，于是测试就写成 12 得到 1。

20
00:01:06.116 --> 00:01:09.846
在我们的运行里，这个测试在候选实现上通过，

21
00:01:09.846 --> 00:01:11.978
在正确的修复上反而失败。

22
00:01:11.978 --> 00:01:16.774
错误的结果等于同一个错误的结果，正确的修复倒像是回归。

23
00:01:17.116 --> 00:01:23.078
所以，当旧代码和约定不一致时，要把它记成违反约定的缺陷。

24
00:01:23.078 --> 00:01:26.485
智能体没法靠照抄代码来决定需求。

25
00:01:27.532 --> 00:01:29.537
这一周的循环有固定的顺序。

26
00:01:29.537 --> 00:01:32.776
在生成任何代码之前，先由你定下约定和测试。

27
00:01:32.776 --> 00:01:34.781
然后在候选实现上运行它们。

28
00:01:34.781 --> 00:01:38.174
在我们的运行里，单元检查在空字符串上失败了：

29
00:01:38.174 --> 00:01:40.641
抛出的是 IndexError，

30
00:01:40.641 --> 00:01:43.417
而它期望的是 ValueError。

31
00:01:43.417 --> 00:01:45.114
这就是红色的那次运行。

32
00:01:45.114 --> 00:01:49.278
如果是导入出错，那只是环境问题；而这一次是真正的缺陷。

33
00:01:49.616 --> 00:01:52.788
接下来提出一个修复：先检查长度再取下标，

34
00:01:52.788 --> 00:01:55.801
并且要求恰好是一个 ASCII 数字。

35
00:01:55.801 --> 00:01:59.608
它可能是你在编辑器里写的，也可能是智能体生成的。

36
00:01:59.608 --> 00:02:01.828
无论哪种，它都只是一个提议。

37
00:02:01.828 --> 00:02:05.952
由在未改动的测试上的一次运行来决定；如果仍然是红的，

38
00:02:05.952 --> 00:02:07.379
提议就退回去修改。

39
00:02:07.716 --> 00:02:10.007
在我们的运行里，它变绿了。

40
00:02:10.007 --> 00:02:12.475
但这只是在这套测试上的绿色。

41
00:02:12.475 --> 00:02:17.410
所以要挑战它：用一条属性、一个变异体或一个模糊测试输入。

42
00:02:17.410 --> 00:02:20.230
再加一个能把缺陷区分出来的测试，

43
00:02:20.230 --> 00:02:23.403
并在不改动之前测试的情况下重新运行。

44
00:02:23.403 --> 00:02:29.043
约定和测试归你；智能体只负责提议；每一个提议都由实际执行来判定。

45
00:02:30.082 --> 00:02:34.518
这是一个弱测试，写法很像智能体可能给出的样子。

46
00:02:34.518 --> 00:02:38.569
它用 3 调用解析函数，然后做了三条断言。

47
00:02:38.915 --> 00:02:42.401
即使结果是错的，这三条断言也全都通过。

48
00:02:42.401 --> 00:02:47.539
每一条为什么看不出错误，正是本周活动要你解释的第一件事，

49
00:02:47.539 --> 00:02:49.190
所以这部分留给你。

50
00:02:49.532 --> 00:02:50.563
下面是证据。

51
00:02:50.563 --> 00:02:54.685
用课程示例代码里的一个错误解析函数来跑这个测试：

52
00:02:54.685 --> 00:02:56.746
它对每个数字都返回 0。

53
00:02:56.746 --> 00:02:58.807
这三条断言照样全部通过。

54
00:02:58.807 --> 00:03:03.617
只有调用本身崩溃时，这个测试才会失败；它从不检查返回值。

55
00:03:03.617 --> 00:03:08.770
而且，对一个测试的自信解释，并不能证明它真的检查了任何东西。

56
00:03:09.815 --> 00:03:13.323
属性描述的是一整族输入都要满足的规则。

57
00:03:13.323 --> 00:03:15.909
对 0 到 9 的每个数字，

58
00:03:15.909 --> 00:03:21.818
解析它的字符串都必须返回范围内的整数，而且能还原成同一个字符串。

59
00:03:21.818 --> 00:03:25.511
这个合法输入域很小，可以完整地检查一遍。

60
00:03:25.848 --> 00:03:29.886
一个永远返回 3 的解析函数，能通过那一个示例，

61
00:03:29.886 --> 00:03:31.400
却通不过这条属性。

62
00:03:31.400 --> 00:03:34.766
但是，有缺陷的候选实现仍能通过这条属性。

63
00:03:34.766 --> 00:03:36.785
拒绝的行为需要单独检查。

64
00:03:37.831 --> 00:03:44.135
变异测试问的是一个更尖锐的问题：如果代码里有缺陷，测试会发现吗？

65
00:03:44.135 --> 00:03:49.453
从一个能通过测试套件的正确解析函数出发，做三处小改动。

66
00:03:49.798 --> 00:03:55.484
对所有合法输入都返回 0；只对 9 返回 0；接受尾随字符。

67
00:03:55.484 --> 00:03:58.137
用同一套测试分别去跑每一个。

68
00:03:58.137 --> 00:04:02.685
只要有断言发现了改动，这个变异体就被“杀死”了。

69
00:04:03.031 --> 00:04:05.965
在我们的运行里，弱测试一个都没杀死。

70
00:04:05.965 --> 00:04:08.573
单元测试加上属性，三个全部杀死。

71
00:04:08.573 --> 00:04:12.485
存活的变异体说明，有一处改动是这套测试看不见的。

72
00:04:12.485 --> 00:04:15.419
它会一直算作存活，直到有测试杀死它，

73
00:04:15.419 --> 00:04:18.679
或者有一条写明的约定理由说明它是等价的。

74
00:04:18.679 --> 00:04:23.569
而且，这只是课程里三个固定变异体的计数，不是一个通用的分数。

75
00:04:24.614 --> 00:04:31.798
变异测试报告要写明工具和配置、原始版本、测试、变异体和分母。

76
00:04:31.798 --> 00:04:38.743
超时、无效的变异体或者环境坏了，都不算被杀死，要单独诊断。

77
00:04:39.081 --> 00:04:40.904
有些变异体是等价的。

78
00:04:40.904 --> 00:04:46.192
只改 ValueError 的提示文字：约定没有规定措辞，

79
00:04:46.192 --> 00:04:48.197
所以不应该有测试失败。

80
00:04:48.197 --> 00:04:52.755
在我们的运行里，它在单元测试和属性检查下都存活了。

81
00:04:52.755 --> 00:04:55.126
把它排除，并写下这个理由。

82
00:04:55.464 --> 00:04:57.460
但绝不能为了让数字好看，

83
00:04:57.460 --> 00:05:01.286
就去掉一个确实改变了行为、却存活下来的变异体。

84
00:05:01.286 --> 00:05:06.610
行覆盖率记录的是哪些代码执行过；变异测试问的是改动有没有被发现。

85
00:05:07.647 --> 00:05:10.480
模糊测试支架使用一个固定的种子。

86
00:05:10.480 --> 00:05:12.427
它先试九个边界字符串，

87
00:05:12.427 --> 00:05:17.029
再从固定字符表里随机生成一百个最长四个字符的字符串。

88
00:05:17.029 --> 00:05:21.986
每个结果都和一张单独的表对比，表里是十个合法输入的答案。

89
00:05:22.331 --> 00:05:25.756
用种子 7，第一个发现是空字符串，

90
00:05:25.756 --> 00:05:29.382
抛出了意外的 IndexError。

91
00:05:29.382 --> 00:05:32.606
它来自边界列表，而不是随机搜索。

92
00:05:32.947 --> 00:05:38.524
单独回放它，保留这个确切的输入，在修复之前把它加为回归测试。

93
00:05:38.524 --> 00:05:44.287
要保留输入本身，因为生成器或运行环境一变，光靠种子未必能复现。

94
00:05:44.287 --> 00:05:49.863
也回放一下 12：模糊测试既要抓住崩溃，也要抓住错误的结果。

95
00:05:50.897 --> 00:05:56.517
智能体也可以提出测试支架本身：一个属性策略、一条变异命令，

96
00:05:56.517 --> 00:05:59.037
或者一个小的模糊测试支架。

97
00:05:59.037 --> 00:06:01.362
把它当作主张和候选代码。

98
00:06:01.362 --> 00:06:05.432
绝不能让它自己决定分数，或者修改打分工具。

99
00:06:05.780 --> 00:06:11.015
假设提出的测试支架没有测试判定器：它捕获所有异常，什么都不记录。

100
00:06:11.015 --> 00:06:15.595
在我们的运行里，它在已知有缺陷的候选实现上报告零个发现，

101
00:06:15.595 --> 00:06:17.067
在修复上也是零个。

102
00:06:17.067 --> 00:06:18.213
它分不出两者。

103
00:06:18.213 --> 00:06:22.302
示例代码自带的测试支架，在候选实现上报告了六十个，

104
00:06:22.302 --> 00:06:23.938
在修复上一个也没有。

105
00:06:24.280 --> 00:06:26.807
所以，在相信一个测试支架的沉默之前，

106
00:06:26.807 --> 00:06:30.458
先用一个已知有缺陷的目标和一个修复后的目标都跑一遍。

107
00:06:30.458 --> 00:06:33.266
由实际运行的结果来决定一个主张能否成立。

108
00:06:34.312 --> 00:06:35.522
回到这个循环。

109
00:06:35.522 --> 00:06:37.941
修复版本先检查长度再取下标，

110
00:06:37.941 --> 00:06:41.223
并且要求恰好是一个 ASCII 数字。

111
00:06:41.223 --> 00:06:43.469
原封不动地重跑原来的测试。

112
00:06:43.469 --> 00:06:46.579
在我们的运行里，单元测试和属性通过，

113
00:06:46.579 --> 00:06:51.762
模糊测试在它所列的输入里没有发现任何问题，两个回放也都通过。

114
00:06:52.096 --> 00:06:53.760
然后再挑战一次绿色。

115
00:06:53.760 --> 00:06:56.755
问一问：还有什么缺陷能通过这些检查？

116
00:06:56.755 --> 00:07:02.080
加一个能把它区分出来的测试，并在不改动之前测试的情况下重新运行。

117
00:07:03.129 --> 00:07:05.807
所以，绿色只能代表这些检查试过的东西。

118
00:07:05.807 --> 00:07:09.190
这里每一种检查都覆盖了一组明确列出的输入或改动。

119
00:07:09.190 --> 00:07:13.700
要说清楚覆盖的是哪些，因为没有哪一种能证明这个解析函数是正确的。

120
00:07:13.700 --> 00:07:18.210
而且，一次绿色的运行，并不能代替你自己对每一次失败和通过的解释。

121
00:07:18.546 --> 00:07:19.675
试一个问题。

122
00:07:19.675 --> 00:07:25.321
一条属性检查的是：对生成的数字字符串，解析出的值都不是负数。

123
00:07:25.321 --> 00:07:28.145
它覆盖了负数或带装饰的输入吗？

124
00:07:28.145 --> 00:07:31.909
写下预期的行为，以及你接下来会跑的检查。
