聊完测试金字塔,让我们回到单元测试。
在这篇中,我们会从单元测试的控制与撰写开始,一路带到单元测试与「单一职责原则(Single Responsibility Principle)」的关系。
我们来看一段虽然有点违反单一职责原则,但暂时不算太严重的程序,体验一下测试该怎麽加。
我们回到我们的教务处网站范例,教务处打算要发奖学金了,来申请奖学金的同学只要本学期「至少 1/2 的学科有达 80 分以上,就发给全额奖学金 1 万元,如果只有 1/3 达成,就发半额的 5 千元。」
这样的需求对各位读者来说应该算简单,我们可以马上来做看看:
首先,想要算奖学金,必须要先有成绩单,於是我们先准备好成绩单与各科目成绩如下:
@Data
@AllArgsConstructor
public class Course {
private String name;
private int score;
}
@Data
public class Transcript {
private List<Course> courses;
public Transcript(Course... courses) {
this.courses = Arrays.asList(courses);
}
}
有了成绩单以後,我们就可以拿着成绩单,直接把需求指定的逻辑给做出来了,别忘了考虑「本学期未修课」的案例:
public int calculate(Transcript transcript) {
List<Course> courses = transcript.getCourses();
if (courses.isEmpty()) return 0; // 不修课跟人家领什麽奖学金!
int total = courses.size();
int achieved = 0;
for (Course course : courses) {
if (course.getScore() >= 80) {
achieved++;
}
}
double rate = (double) achieved / total;
if (rate >= (double) 1 / 2) {
return 10_000;
} else if (rate >= (double) 1 / 3) {
return 5_000;
} else {
return 0;
}
}
这里,读者也许已经注意到了,这里写程序的顺序是「由下往上」的设计,也就是先准备底层物件,再做上层逻辑。这其实跟笔者平常的习惯是反过来的,但这是为了表现其实不一定要由上往下,也不一定要先写测试。至於「由上往下」有什麽不一样,我们晚点讲 TDD 的主题时,会再多做说明。而对单元测试的初学者来说,我们不要一次改变太多写程序的习惯,一步一步慢慢来,首先先从「程序都要尽量带测试」开始。
节录自电影「让子弹飞」
程序写完就来测试吧。我们首先处理「没有修课的人」,没修课你跟人家领什麽奖学金?所以预期他应该要得到零元。
@Test
void NO_courses() {
ScholarshipService service = new ScholarshipService();
int actual = service.calculate(new Transcript(/*nothing*/));
Assertions.assertEquals(0, actual);
}
再来,如果这学生很厉害,真的有达成「全额奖学金」的目标,那我们就要依约给他一万元:
@Test
void full_scholarship() {
ScholarshipService service = new ScholarshipService();
int actual = service.calculate(new Transcript(
new Course("Algorithm", 70),
new Course("Computer Internet", 80),
new Course("Operating System", 90)
));
Assertions.assertEquals(10_000, actual);
}
接着,我们再依样画葫芦,把半额奖学金跟未达标的测项补上就好。完整测项请参阅 https://github.com/bearhsu2/ithelp2021.git
以上,我们就介绍了如何透过控制依赖,来控制受测对象的程序逻辑,以达到分别测试各个不同逻辑分支的效果。接下来我们多花一点篇幅来探讨,当程序变得复杂,会对我们的测试造成什麽影响。
上述的程序在笔者工作场合,Code Review 是不会过的,Code Smell 太重了。但是在需求不会修改的情况下,我觉得还可以,是因为命名还算清楚,篇幅也还算小,阅读上没什麽问题,测试也不难加。然而,需求哪有不会修改的呢?
有一天修改的需求来了。我们必须把学生分为「大学生」、「硕士生」,与「博士生」三种,全额奖学金的金额与资格判定方式都不同:
大学生 | 硕士生 | 博士生 | |
---|---|---|---|
全额奖学金 | 10,000 | 15,000 | 40,000 |
全额条件 | 至少 1/2 的学科有达 80 分以上 | 依学分加权平均後达 90 分以上 | 全部学科达 90 分以上 |
半额条件 | 至少 1/3 的学科有达 80 分以上 | 依学分加权平均後达 80 分以上 | 全部学科达 80 分以上 |
如何?是否困难度一下子就飙升了?先不说程序了,我们光来数看看测项数吧!要达到可信赖的测试保护力,至少一个逻辑分支要有一个测项保护,这不为过吧?而逻辑分支主要取决於变因。我们来看看这样的需求,需要几个测项:
变数 | 分支数 | 说明 |
---|---|---|
学生种类 | 3 | 大学、硕士、博士 |
奖学金资格 | 3 | 全额、半额、资格不符 |
上述可能发生的情况,再加上三种学生都有可能「没修课」,至少你就要写 3 x 3 + 3 = 12 个测项!
碍於篇幅,这里我就不附上完整程序码了,文末的 GitHub 连结中有完整范例,其中程序就将近 100 行,测试更是飙升到超过 200 行!读者可以下载来看看,体会一下「老板的简单两句话,我们要多做多少事」...
「到底为什麽这麽麻烦啊?我到底做错了什麽事啊?」
Uncle Bob 在 Clean Code 谈到函式时表示:「函式应该要做一件事。它应该要把这件事做好,并且只能做这件事。」书中也另外补充说明道:「一个函式应该要只能因为一个理由而被修改。」
为什麽?我们知道,「修改乃是 bug 发生的源头」,线上运行得好好的程序,会突然出 bug,有 87% 是因为修改所致。当你一段函式要管非常多事情的时候,这段程序就很有可能因为某个需求的变动而被修改。原因很简单:「因为他胖嘛~下雨淋到胖子很正常,因为他面积大呀!」
问题一段函式已经够长了,我好不容易读了半小时终於看懂他在干嘛,并且找到修改点,这时你会做什麽事?要知道,一段程序不管谁写的,只要你把它改坏,他马上就变成你的了。这时为了自保,聪明如你,会做的事就不外乎两个:
问题来了,不管你决定要做这两件事的哪一件,你都又让这段程序多做了一件事,也就是说,他又管了更多事,变得更复杂了。是不是很有既视感?是啊!我们每天在面对的程序,就都是长这样啊!这是个很明显的恶性循环啊!
有了职责的概念後,我们来看看刚刚这段程序管了哪些事?
随便列就四个责任,真的蛮多的。
如果这时,三种学生再各自加上「侨生」、「体保生」、「身心障碍」等变因,你要再多列多少测项?如果你继续一股脑儿往 calculate() 函式加逻辑,继续加 if 或复制贴上,你应该可以预期一个「指数成长」的程序行数与测项数。坏消息还不只这个。你知道需求是不会停止成长的,所以,一旦你放任你的程序充斥这种「太胖所以容易淋湿」的程序码,你就会读得很痛苦,改得很痛苦,并且测得很痛苦。
可是你又不笨,对吧?读跟改,你就避不了,但测可以往外推啊!於是解决难测的办法,就是赶快写完赶快丢给 QA 测,万一 QA 没抓到 bug,就请用户帮忙测噜。那就回到我们第一天讲的,全公司大家都有意无意地在「努力降低品质」的窘境了。
程序管太多就不好测,不好测就要测很久,要测很久就很急,急就复制贴上,然後程序就管更多。你现在手上的专案就是这样。如果想要打破这种恶性循环,那你就应该要在问题开始产生的初期,就把程序码「重构」好,把权责区分好。结构好的程序,可以让工作量呈现「线性成长」,而非「指数成长」。
怎麽重构,晚点我们有一些聊「重构」的章节,会回头看看怎麽处理。但这里先不急着讨论,为什麽?因为如果你到时候真的很想重构,那此时此刻你应该做的事,是要先加上足够的测试。有了测试的保护,你才有重构的勇气,就不用向梁静茹要了。
图片来源:维基百科
请注意,这里我们都没有在讲 TDD 喔!我们就纯粹是要为一个「已经存在的程序」加上测试保护逻辑而已。
谜之声:「胖不是病,但胖起来要人命。程序跟人都一样。」
ithelp2021
>>: Varying - fragment shader 之资料
[讲古时间]: 回忆过去~ 痛苦的相思忘不了~ 哈哈,记得刚出社会时,虽然说是张白纸,不过可能太白...
作业系统L7-死结 死结特性 互斥:一次一个行程占用资源 占用与等候:至少一个行程占用资源且正等待其...
Day03 - 纯 Html 复杂型别 object 复杂型别定义 复杂型别我拆成三篇 object...
●Java 自定义注解 创建自定义注解类似於编写接口,不同之处在於interface关键字以@符号为...
《30天带你上完 Google Data Analytics Certificate 课程》系列将...