工程质量-06-第二阶段:以测试为核心的基本流程
01 Feb 2022 • 4 min read建立了代码审查制定之后,会马上遇到一个关键的问题:质量到了一定程度上很难提升了。代码审查的质量严重依赖于参与者的投入程度,但即使是最认真的严格的审查者,也有状态不好的时候。我们虽然可以定制一系列的代码审查规范,代码编写规范或者再引入一些静态检查工具,这一些或多或少会对代码质量有一定的提升,但是这些机制并不能够根本解决人在审查过程中效率的客观不稳定性。
因此在第二阶段,我们会引入以测试为核心,特别是单元测试为核心的流程。
以下我都会使用单元测试来代指测试,实际上单元测试还包括集成测试、端到端的测试三类测试,本质上是一样的,只是处理的问题范围大小不同。
graph LR
A[测试]-->B[单元测试-Unit Testing]
A-->C[集成测试-Integration Testing]
A-->D[端到端测试-End to End Testing]
这一个阶段引入的单元测试之后,缺陷清除率如下所示
Removal Step | Lowest Rate | Modal Rate | Highest Rate |
---|---|---|---|
非正式的设计审查(Informal design reviews) | 25% | 35% | 40% |
非正式的代码审查(Informal code reviews) | 20% | 25% | 35% |
静态代码分析(Static code analysis) | 5% | 10% | 15% |
单元测试(Unit test) | 15% | 30% | 50% |
集成测试(Integration test) | 25% | 35% | 40% |
系统测试(System test) | 25% | 40% | 55% |
缺陷排除预期累计效率值 | 72.74% | 88.02% | 95.52% |
6.1. 为什么是单元测试
如果五年前,让我来来回答如何提高工程质量,我一定不会将单元测试放在这里。我直觉的认为单元测试需要两个很重要的缺陷
- 单元测试需要花很多时间。少量的单元测试意义并不是需要大,要想让单元测试达到一个可用的效果需要投入大量的时间,这可能比开发编码的时间还长,这看起来是得不尝失的。不仅仅产品、市场很难接受,研发人员本身也很难接受这种投入。
- 单元测试依赖的代码系统架构需要是稳定的,不会有特别强烈的变化,基本架构变了,配套的单元测试也需要变化,维护单元测试将会变得很难。现在我们的软件迭代都非常快,没有真正稳定的架构,变化本身是一种需求。
但是随着对研发本身以及单元测试的深刻了解,我的观念有了根本性的变化。单元测试有几个非常重要的好处
6.1.1. 单元测试可以有效的降低缺陷率
降低缺陷率是我们写单元测试的最本质的驱动力。
- 当我们写了一段业务代码之后,需要通过测试来验证它们是否能够满足逻辑需求。这种情况下就需要测试,单元测试是其中的一种。也是最重要的一种。
- 当我们调整,重构了某一块业务代码的时候,需要通过单元测试来保证目前的调整并没有引起其它的问题,这样才能够给我们信心。
6.1.2. 单元测试可以节省研发时间
这个观点与我最初的直觉是相反的。单元测试节省研发时间可以从两个方面来看。
首先,一个正常的项目,一般的生命周期至少两到三年起步,这中间会迭代无数次的版本,代码量也会越来越多。如果在这无数次的版本迭代过程中
- 有了单元测试,我们可以保证每一次在添加新功能的时候,不会影响到原本的功能。
- 有了单元测试,我们可以确保我们在重构了某一些旧的功能模块之后,不会影响到现有的业务逻辑。
这一点会极大的影响我们的效率。如果我们没有这样的机制,我们就没有办法从根本上避免一些问题的反复出现。如果我们将问题全部丢给测试、丢给代码审查的话。那么我们其实很难保证新开发的功能是否影响了原有的功能,新修改的BUG是否又引起了新的BUG。这需要投入更多的测试资源和时间来保证。对于单次具体的研发过程而言,单元测试可以有效的降低提交测试的BUG率。这会让测试这边少花时间和资源,这会极大的节省时间。
6.1.3. 单元测试可以有效的保持代码结构合理性
单纯的随便写写单元测试并不难,但是想要写好、管理好单元测试,让单元测试发挥它的功能作用,是一件非常难的事情。后面在如何写单元测试这一部分我会详细说明这里面的一些难点和最佳实践。
好的单元测试是对整体的代码结构是有很高的要求的。我们学习过很多关于如何写好代码的理论规范,比如说尽量不要有重复代码、一个函数不要过长、函数的参数不要过多,模块与模块之间高类聚、低耦合、面向对象的SOLID原则、23种设计模式… …等等。
这些规范和最佳实践听起来都很有道理,但是在实际写代码的过程中发现,我们大部分编程都是面向解决问题的思维。很多时候只要满足产品需求,其它的约束并不是那么重要,只有在我们的代码已经乱成一团遭的时候,才会想到当时要能够多规范一下多好。
这些都是隐性的约定,都是对编程者的道德约束,并不是法律要求。在能够解决问题的前提下,要求过多,反而会有吹毛求疵、故意找茬的不利影响。更无赖的是,有一些编程者使用了这些机制,但是其他人不理解,会反而觉得有过度设计、炫技的感觉。
造成这种问题的核心原因是我们没有将单元测试当成一个必须的功能需求来完成它。如果项目里面要求写完整的单元测试,你就会发现,上面讲的那些理论和设计模式真的是很有用。你都不需要刻意去学习这些理论,只需要为一个项目完整的写单元测试,抱着完成单元测试这个需求的角度,这些理论自然就应用上了。
业内其实有很多资料都在强调单元测试的重要性
- 《重构-改善既有代码的设计》[13]这一本书其实都是在讲的如何写单元测试
- 现代软件开发的几种模式BDD、TDD、ATDD都是基于测试来进行开发的
- 《Clean Code》[15]里面,对于维护不好的代码定义就是:没有单元测试的代码。
6.2. 单元测试的理论分析
要写好一个单元测试并不简单,这里面涉及到大量的权衡,单元测试会对业务侧的代码与模块之间的关系和实现有非常深刻的影响。对于单元测试我强烈推荐一本由Vladimir Khorikov于2020年写的<Unit Testing Principles, Practices, and Patterns>[14] ,这一本书从理论上详细的回答了:什么是好的单元测试、怎么评估一个单元测试,怎么样才能够写好单元测试这些基本问题。下面的部分很多都是来自于这一本书的内容。
一个好的单元测试,它会有四个重要的特征
- 预防BUG(Protection against regressions):预防BUG,这个是单元测试最本质的功能。
- 添加一个新的功能,可以通过写单元测试来验证整体的功能是否能够符合预期。
- 当项目代码越来越多的时候,经常性的会重构优化某一些模块,在这种情况下,拥有良好的单元测试机制,对保持整个软件开发的稳定性非常重要。
- 抗重构(Resistance to refactoring): 重构是软件生命周期里面最普遍的需求
- 一个好的单元测试一定是需要抗重构的,在重构过程中提供有利的条件,而不是每一次重构都需要重写单元测试。
- 抗重构最重要的本质是不乱报问题。我们在重构了某一些模块之后,以前写的单元测试,并不会乱报问题(本身代码没有问题,但是单元测试却没有通过),这会极大的影响研发对单元测试的信心。最终会导致单元测试的机制变成负担。
- 快速反馈(Fast feedback) :
- 单元测试要快,可以并行跑,相互之间不会有什么影响。
- 整体运行非常方便,尽量不需要依赖额外的环境才能够跑起来。这样的单元测试价值最高。
- 可管理性(Maintainability)
- 单元测试需要易于管理,单元测试的代码本身不能够搞得太复杂,代码本身是负债。单元测试的代码也是代码,所以单元测试需要越简单越好,简单到很难出现BUG。不要把单元测试搞复杂了,在单元测试里搞起了BUG,那样得不尝失。
6.2.1 单元测试的精度指标
预防BUG(Protection against regressions)、抗重构(Resistance to refactoring)这两个要素可以用来评估单元测试的精确度。如下所示
- Correct inference - true negatives。如果功能本身没有问题,单元测试通过了,那么就是一次成功的单元测试。
- Correct inference - true positives。如果功能本身有问题,单元测试检查没有通过。这也是一种很好好的单元测试。
- Type I error - false nagative。如果本身功能有问题,但是单元测试并没有检查出来,通过了。那么这种情况下可能就需要补充单元测试。这个指标在研发的当下实际上是很难统计。
- Type II error - false positive。如果功能本身并没有问题,但是单元测试却报出了问题。那么这种情况下,单元测试可能已经不适合现在的代码了。证明目前的单元测试抗重构的能力并不强。
在这种情况下,我们可以定义单元测试的精度,根据机器学习里面的理论我们可以这样定义。
但是很多时候,作者在书里面这样定义的精度
6.2.2. 单元测试的约束性关系和得分
单元测试能够完全满足上面的四个属性么?答案是不能。在上面四个属性里面有三个属性是有制约关系的。
如上图所示
- 如果我们强调:预防BUG和抗重构这两个属性,那么这种情况的极端就是写了一个端到端的测试。(后面会讲什么是端到端的测试)
- 如果我们强调:抗重构和快速反馈,那么我们写的是一个没有特别什么用的trivail 测试。
- 如果我们强调:预防BUG和快速反馈,那么我们写的是一个Brittle tests。
存在同时有三种特性的单元测试么?并没有,那么永远并不能够达到的领域。这和数据库领域里面的CAP原则是一样的,你只能牺牲其中一点,扩大其它两个属性。
除了上面的关系之外,还有如下的关系
- 三个属性中的某一个属性不能够完全为0。任何一个属性缺失,就代表这个单元测试是没有什么实际意义。
- 抗重构是一个二分的选择。要么有抗重构的能力,要么没有抗重构的能力。因此,我们在写单元测试的时候,抗重构是必须要保持的。剩下的只能够在预防BUG和快速反馈两个属性之间进行权限。这两个属性又具有如下的关系
- 可管理性,和抗重构一样,这是我们在写单元测试都必须要一直保持的,不能够妥协。
上面的图,我们可以清楚的看到。越选择快速反馈,这个测试就越像单元测试。越选择预防BUG,这个测试就越像端到端的测试。在中间水平的就是集成测试。
要衡量一个单元测试的评分,可以将上面的四个维度相乘。这也算是一个评估方法,这种方法告诉我们,上面四个点,不能够放弃任何一点。任何一个方面得零分,这个单元测试就得了零分。
6.2.3. 单元测试覆盖率
提到衡量单元测试,我们会天然的想到使用单元测试的覆盖率来衡量。单元测试的覆盖率一般有两类。
- 代码覆盖率:就是单纯的验证测试单元的代码是否已经被全部执行了。
- 代码分支覆盖率:验证测试单元里面的各个分支是否已经被全部跑过。
覆盖率是一个可以用证伪,但是不能够用来证真的指标。例如下面的代码
bool IsStringLong(const std::string input) {
return input.size() > 20;
}
// Unit Testing
void Test_String_Too_Long() {
std::string a = "abc";
bool res = IsStringLong(a);
ASSERT(res == false);
}
上面的IsStringLong
的实现非常简单,下面的单元测试也验证了一种情况。从代码覆盖率来讲,已经达到100%了。但是实际上,这个单元测试只是验证了一种情况,还有大于20个字符的情况没有验证。
覆盖率只能够证伪不能够用来证真意味着。如果一个项目的单元测试覆盖率低于50%,这一点可以证明这个项目的单元测试一定是不够的。但是当一个项目的单元测试覆盖率达到90%,并不能够证明这个项目的单元测试已经很完善了。
6.3. 单元测试的最佳实践
前面说过,单元测试的一个最重要的优点之一是可以让整体的代码结构变得更好。一个易于单元测试的代码整个的风格将会是优雅的。这里面就会涉及到很多关键点。
6.3.1. 测试的内容:有重点的测试
单元测试的第一个最佳实践就是需要测试那些投入产出比非常高的代码里面。一个项目里面的代码按照代码复杂程度和与第三方依赖程度,可以划分为四类。
- 不重要的代码(Trivial Code):这一类代码就像上面举的例子一样,它的功能实现非常简单,可能只有一句话。这一样的功能函数是不需要进行单元测试的。单元测试的价值并不大。
- 领域代码和算法功能模块(Domain model, algorithems)。这是属于业务逻辑和核心算法层面的代码。它们里面有比较复杂的业务逻辑在其中,但是一般不会涉及到与第三方交互。
- 与第三方交互的功能(Controllers):主要负责与第三方系统进行交互的,例如数据库、网络、文件系统、消息队列、SDK等等。
- 过度复杂的代码(Overcomplicated code):这些代码里面即有比较复杂的业务逻辑,又涉及到与第三方系统之间的交互。这是整个系统里面最复杂的代码。
如何对这些代码模块进行单元测试呢?
- 不重要的代码(Trivial Code):这一类代码比较简单,不需要测试
- 领域代码和算法功能模块(Domain model, algorithems)。这一类代码是测试的核心部分
- 与第三方交互的功能(Controllers):这一类代码也需要重点测试
- 过度复杂的代码(Overcomplicated code):这一类代码过多,证明这个模块设计是有一些问题的,需要从设计和代码结构上将这一类代码简化,让这一类代码变成领域代码和算法功能模块(Domain model, algorithems)和与第三方交互的功能(Controllers)分别进行测试
测试领域模型的代码,由于不与第三方进行交互,这一部分一般称为单元测试。而测试与第三方交互的代码测试的时间比较长,跨度的内容比较多。这些测试一般被称为集成测试
6.3.2. 测试的内部结构-AAA结构
单元测试它有三个特点
- 只验证一个业务功能行为。
- 能够执行得够快
- 与其它的单元测试之间是独立的,相互之间不受影响
对于一个项目来说,代码是负债,只要写过的代码都是需要维护的。单元测试的代码也是代码,也是需要维护。一个单元测试的代码应该需要非常简单,简单到它不应该出什么问题,只有这样,维护单元测试才不会有压力。单元测试的代码不需要什么创新,直接同一种模式就可以了。
现在的单元测试最佳实践就是一个AAA结构。一个单元测试就分为三个部分
- 构建-Arrange:初始化单元测试的各项参数
- 执行-Action:执行单元测试
- 断言-Assert:检查测试结果
如下代码所示,下面就是一个简单的单元测试
int Sum(int a, int b) {
return a + b;
}
void Testing_Sum_of_Two_number() {
// Arrange
int a = 10;
int b = 20;
// Action
int res = Sum(a, b);
// Assert
ASSERT(res == 30);
}
上面的单元测试就是一个典型的AAA模型。使用AAA模型还有几个注意的点
- Arrange部分:应该是这三部分里面代码行数最多的部分。但是如果一个单元测试需要初始化的东西太多,那么从一个方面证明这一块代码设计其实是有问题的,需要考虑重构。
- Action部分:正常情况下就应该只会有一句执行代码,这样保证这个单元测试测试的内容能够足够的小,精确。如果一个单元测试的Act部分超过一行,证明这个单元的代码设计可能有问题,要使用这个模块的隐含调用有点多。可能需要考虑重构。
- Assert部分:应该只去验证结果,而不要关注测试里面的细节。关注结果能够保证未来在重构Sum这个函数的时候,这个单元测试不需要变,它得到的结果也是可以用的。Assert部分也不应该过多,过多就证明单元测试关注的细节太多了,也有问题。
在有一些场景之下,一次测试可能会有多个流程,很多人天然的可能会将单元测试写成如下的形式
除了实现没有办法的情况下,不要使用这种形式。出现这种情况优先建议将一个单元测试拆分成多个单元测试来处理。还是那一个原则,单元测试一定要简单,简单到连变量的命名都使用同一个。这样的单元测试不容易出问题,同时所有人都很容易看懂。
6.3.3. 只验证结果,不关注实现细节
单元测试测试的是一个业务行为,而不是限于一个简单的函数。简单的函数如前面举的Trivail Test一样,测试的价值不大。它本身实现非常简单,出问题的概率特别小。所以,单元测试以业务行为为角度进行测试,一次测试内部可能会涉及到很多代码,这才是一个单元。
为了保证单元测试能够抗重构,我们在测试的时候需要只关注结果,而不要关注实现的细节。如下面的代码所示
class RenderHtml {
public:
RenderHtml(const std::string header, const std::string body, const std::string foot){
msg[0] = header;
msg[1] = body;
msg[2] = foot;
}
std::string Render() {
std::string res = "<h>" + msg[0] + "</h>";
res += "<body>" + msg[1] + "</body>;
res += "<i>" + msg[2] + "</i>";
return res;
}
public:
std::string msg[3];
// std::string header_;
// std::string body_;
// std::string foot_;
}
void Testing_Normal_Render() {
// Arrange
std::string header = "H";
std::string body = "B";
std::string foot = "F";
RenderHtml sut(header, body, foot);
// Act
std::string res = sut.Render();
// Assert
ASSERT(res == "<h>H</h><b>B</b><i>F</i>");
ASSERT(sut.msg[0] == header);
ASSERT(sut.msg[1] == body);
ASSERT(sut.msg[2] == foot)
}
上面的测试中,在Assert阶段,一共验证了四个部分。第二到第四部分的代码去校验了RenderHTML里面的实现细节。这样的单元测试是非常利于于重构的,如果未来需要对RenderHTML进行重构,那么这个单元测试就跑不起来了。只有第一部分的验证是正确的,只去关注结果,不要关注细节。
上面这个代码看起来很蠢,我刻意的定义了一个msg[]
数组,用来演示细节的问题,正常情况下很少有会这样实现和测试,但是这是一个现象,很多人写单元测试的时候会自然的想验证越多的结果越好,越精确,这样关关注于实现,反而会起到相反的副作用。
这是一个示例,我们需要关注的是结果,而不是关注内部的实现细节。这样的单元测试才是好的单元测试,才能够提供最正确的结果。
6.3.4. 各类测试的占比
前面的内容你已经知道了,测试按照测试的范围不同,包含了三类测试
- 单元测试(Unit Testing):有三个特性:一次只验证一个比较简单的业务行为,没有外部依赖,各测试之间是完全独立的。
- 集成测试(Intetration Testing):只要不符合单元测试的三个特性的都是集成测试:
- 如果验证的业务行为比较复杂,涉及到的代码比较多,也可以称为集成测试。
- 如果测试过程中有外部依赖,比如数据库、消息队列、网络、文件系统访问等也属于集成测试。
- 如果涉及到外部依赖是只属于这个测试的,那么可以通过Mock技术来进行模拟
- 端到端测试(End to End Testing):端到端的测试其实是集成测试的一个子集,它也是执行时间比较长,一次执行会涉及到更广泛的业务,一般都会涉及到与第三方的集成,站在用户和使用的角度了进行的测试。这种测试的最典型的特征是不需要使用Mock来模拟第三方依赖。
这三类测试在一个完整的项目中占比应该是显现一个金字塔结构,如下图所示
也就是说,项目中大部分的测试应该是单元测试,其次有一部分是集成测试,最后才是少量的端到端测试。虽然集成测试和端到端测试验证的业务逻辑比较多,看起来投入产出效率很高。但是这种测试一般情况下执行起来都比较复杂,特别是依赖第三方系统,所以不利于在开发过程中经常跑。
Google公布自己内部统计的结果显示。
单元测试占比80%,集成测试有15%左右,而端到端的测试需要大量的资源调度只点整体测试的5%。所以,正常情况下,优先写最简单的单元测试,单元测试能够保证简单和高效。这一块做好了,集成测试和端到端测试可以放到CICD流程上面集中来做。
这种测试的占比,其实是对项目的代码是有一定要求的。现在的代码经常涉及到与第三方的对接,比如网络、数据库等交互。要想保持这种结构,我们在项目的架构中就需要比较严格的区分领域业务代码与第三方对接者之间的联系。
如上图所示
- 单元测试重点验证的是业务逻辑,这是一个独立的部分。它只管输入和输出,没有第三方依赖。
- 集成测试重点验证的第三方进行交互的部分Application Service。从第三方拿到数据,然后调用领域内的业务逻辑代码处理之后,再发送其它的第三方。
当然也不是所有的项目都需要呈现这样明显的金字塔结构,有一些项目比如大部分的逻辑就是简单的CURD操作数据库相关的。这种情况下集成测试做多一些也没有问题。但是要明白,这是少数现象。
6.3.5. 关于Mock的使用
Mock是单元测试中非常重要的辅助工具,各类语言里面都有相应的Mock工具。Mock主要是将一些依赖的实现Mock一个虚拟的实例,这样方便进行单元拆分隔离测试。Mock工具容易引起的问题是Mock滥用。Mock只去模拟第三方组件库,而且只去模拟那第不能够为这个测试独享的依赖组件
- 单元测试里面不要使用Mock工具。单元测试中不会依赖外部第三方组件,这种情况下你不需要使用到Mock工具。可能有一些人会想,如果不使用Mock那么很多业务逻辑不能够覆盖了。这种情况下你应该更加认真想一想这个功能单元代码有一些分支是执行不到的,那么是不是没有测试完,如果各类测试都无法覆盖到这个分支,那么这个分支是不是有必要的?
- 集成测试中,只Mock那些属于第三方依赖,但是不能够为这个测试独享的实例。比如与第三方共享的SDK,操作这个SDK是有状态的,这种情况下需要进行Mock,来模拟SDK。其它情况下都不需要使用Mock。
- 端到端测试,不需要使用Mock,端到端测试就是测试的完全的业务逻辑。直接在尽量真实的测试环境里面跑并不需要使用Mock来模拟某一些组件。
类与类之间可以使用虚拟类来进行解耦,接口与接口之间可以使用回调函数来进行解耦。Mock可以实例化一个虚拟类,这样可以方便测试。也可以在测试中集成实现一个类来替代Mock技术,实现更深度的检查。
Mock技术只检查一个函数的调用次数,不要检查其它的细节。细节检查越多,就越无法对抗未来的重构。
6.4. 引入单元测试的步骤
单元测试在国内的普及度并不高,要做好单元测试第一步是需要在团队内将单元测试的相关理论拉齐,在这个基础之上再结合实践逐步的完善。
在单元测试的理论方面,我依然强烈推荐Vladimir Khorikov写的<Unit Testing Principles, Practices, and Patterns>一书。能够系统性的集体学习分享一下是最好的。如果没有条件,我前面介绍的单元测试相关内容其实已经足够能够涉及到初步的理论了。在这个基础之上,可以结合《重构-改善既有代码的设计》这一本书里面讲的一些方法来进行逐步的添加单元测试。
6.4.1. 什么时候加入单元测试
这里面主要分为两种情况,如果你现在负责的是一个全新的项目。希望你能够从头就开始,这一部分可以考虑使用测试驱动开发(TDD)的红、绿、重构三个步骤。
- 红:先写一个测试-对应该空实现-这个测试会失败
- 绿:将空实现完成-通过测试
- 重构:再将实现进行优化
对于已经存在的代码,显然不建议进行大规模的添加单元测试,这很难实现。前面的理论已经讲过,单元测试会对代码的整体结构有深刻的影响和要求。如果这个项目以前没有怎么考虑单元测试,现在突然想添加单元测试,涉及到的修改将会是特别大的。
在这种情况下,可以根据《重构》这一本书描述的事件点来进行逐步添加单元测试,随着项目的不段迭代,最终将这个项目的单元测试完成
- 在添加新功能的时候进行重构:每一次添加一个新功能的时候,可以像上面TDD的开发形式一样先写单元测试。这样能够保证功能更加的稳定。
- 在修改BUG的时候进行重构:遇到一个BUG的时候,解决它的同时就可以写一个单元测试在代码里面,以防止未来出现同样的问题。
- 在代码审查过程中进行重构:如果代码审查中发现一些比较重要的模块的代码可读性、可维护性方面都比较差,可以考虑在合适的时间点将这些模块重构。
6.4.2. 将单元测试也加入到CICD流程中去
每一次代码审查之前,都需要先过单元测试。这样代码审查起来不会关注一些比较小的问题。
单元测试会极大的影响整体的代码结构,这一点对提供整体工程的质量是非常重要的。它并不是简单的能够降低BUG率,更重要的是,通过单元测试对整体工程的调整,我们的整体代码会变得极其有结构。
上面的单元测试理论涉及很多。但是其实单元测试很简单,只要你将单元测试当成一个必须要完成的需求,随着一步一步的实践和探索,你一定会慢慢总结出单元测试的理论的。
这和学习一门语言的语法一样,语法规则比较复杂,但是将这一门需要放到编译器上面,它会告诉你哪些地方语法有问题。多写一些程序,久而久之,这门编程语言的语法就学会了,都不需要刻意的去记忆。
单元测试也是如此,你只要为你的团队定好目标。比如单元测试的覆盖率必须达到85%以上,然后再注意单元测试的四个特性,在代码审查的过程中确保单元测试的四个特性得到了支持。随时时间的推移,单元测试就能够很快的普及下去。
6.5. 成败的关键
- 不要急,要坚持下去,相信单元测试能够给工程质量带来革命性的变化。最初写单元测试的时候可能不知道如何写才好,即使写完了又发现重构了某一些部分一些单元测试也得跟着修改,维护起来很麻烦。但是你要相信软件工程界的实践,单元测试一定有意义。
- 对齐单元测试的理论是成败最重要的关键
引用
- [1] Demarco T, Lister T. 人件[M]. 3. 机械工业出版社, 2016 :22-23.
- [2] McConnell S. Code Complete[M]. 2nd ed.. Microsoft Press, 2004.
- [3] Ariel Assaraf. This is what your developers are doing 75% of the time, and this is the cost you pay[EB/OL]. 2015 https://coralogix.com/blog/this-is-what-your-developers-are-doing-75-of-the-time-and-this-is-the-cost-you-pay/.
- [4] Jones C, Bonsignour O. 软件质量经济学[M]. 1. 机械工业出版社, 2014.
- [5] Fournier C. Software Engineering at Google[M]. First Edition. United States of America:O’Reilly, 2020 :27-42.
- [6] Capers Jones. Software Defect Removal Efficiency[EB/OL]. 2011. https://www.ppi-int.com/wp-content/uploads/2021/01/Software-Defect-Removal-Efficiency.pdf.
- [7] 阿里技术. 如何选择 Git 分支模式?[EB/OL]. 2020-07-10[2022-01]. https://zhuanlan.zhihu.com/p/158463879.
- [8] P. C. Rigby and C. Bird, “Convergent contemporary software peer review practices,” in Proceedings of the 2013 9th Joint Meeting on Foundations of Software Engineering. ACM, 2013, pp. 202–212.
- [9] IEEE Standard for Software Reviews and Audits, IEEE Std. 1028-2008.
- [10] T. Baum, O. Liskin, K. Niklas and K. Schneider, “A Faceted Classification Scheme for Change-Based Industrial Code Review Processes,” 2016 IEEE International Conference on Software Quality, Reliability and Security (QRS), 2016, pp. 74-85, doi: 10.1109/QRS.2016.19.
- [11] A. Porter, H. Siy, A. Mockus, and L. Votta. Understanding the sources of variation in software inspections. ACM Transactions Software Engineering Methodology, 7(1):41–79, 1998.
- [12] Baum, Tobias & Leßmann, Hendrik & Schneider, Kurt. (2017). The Choice of Code Review Process: A Survey on the State of the Practice. 111-127. 10.1007/978-3-319-69926-4_9.
- [13] Fowler M & 熊杰. 重构-改善既有代码的设计[M]. 2. 人民邮电出版社, 2010.
- [14] Khorikov V. Unit Testing-Principles, Practices, and Patterns[M]. 1. Manning Publications, 2020.
- [15] Martin R C. Clean Code-A Handbook of Agile Software Craftsmanship[M]. 1. Prentice Hall, 2008.