chapter1
学习单元测试并不仅限于掌握其中的技术部分,比如您最喜欢的测试框架、mock库等等。单元测试不仅仅是编写测试。您必须始终努力在单元测试上投入的时间上获得最佳回报,尽量减少您在测试上投入的精力,并最大化它们所提供的好处。同时做到这两点并非易事。观察那些已经达到这种平衡的项目是很有趣的:它们毫不费力地发展,不需要太多的维护,并且能够迅速适应客户不断变化的环境
观看达到这种平衡的项目非常有趣:它们轻松地增长,不需要太多维护并且可以快速适应客户不断变化的需求。 看到失败的项目同样令人沮丧。 尽管付出了所有的努力,并且进行了大量的单元测试,但此类项目的进度缓慢,存在许多错误和维护成本。
那就是各种单元测试技术之间的差异。 有些软件可以产生很好的结果,并有助于维持软件质量。 其他人则没有:它们导致测试的贡献不大,经常中断且通常需要大量维护。
本书所学内容将帮助您区分好的和坏的单元测试技术。 您将学习如何对测试进行成本效益分析,以及如何在特定情况下应用适当的测试技术。 您还将学习如何避免使用常见的反模式,这些反模式一开始可能很有意义,但会给您带来麻烦。
但是,让我们从基础开始。 本章简要概述了软件行业中单元测试的状态,描述了编写和维护测试的目标,并向您提供了使测试套件成功的想法。
1.1 单元测试的当前状态
在过去的二十年中,一直在推动采用单元测试。 如此成功的推动使得现在大多数公司都认为必须进行单元测试。 大多数程序员练习单元测试并了解其重要性。 您是否应该这样做不再存在任何争议。 除非您从事的是一次性项目,否则答案是肯定的。
当涉及到企业应用程序开发时,几乎每个项目都至少包含一些单元测试。 此类项目中有很大一部分远远超出了这些项目:它们通过大量的单元和集成测试实现了良好的代码覆盖率。 生产代码与测试代码之间的比率可以在1:1到1:3之间的任何位置(对于每行生产代码,都有一到三行测试代码)。 有时,该比率远高于此比率,达到高达1:10。
但是,与所有新技术一样,单元测试也在不断发展。 讨论已经从“我们应该编写单元测试吗?”转变为 改为“编写好的单元测试意味着什么?” 这仍然是主要的困惑所在。
您可以在软件项目中看到这种混乱的结果。 许多项目都有自动化测试。 他们甚至可能有很多。 但是这些测试的存在通常无法提供开发人员希望的结果。 在此类项目上取得进展,程序员仍需花费大量精力。 新功能需要永远实现,新的错误会不断出现在已经实施和接受的功能中,而应该帮助的单元测试似乎根本无法缓解这种情况。 他们甚至会使情况变得更糟。
对任何人来说这都是一个可怕的局面,这是因为单元测试无法正常完成工作。 好的测试与坏的测试之间的区别不仅在于品味或个人喜好,还在于您正在从事的这个关键项目的成败。
很难过高地讨论进行良好的单元测试的重要性。 尽管如此,在当今的软件开发行业中,这种讨论并不多见。 您会在网上找到一些文章和会议演讲,但是我还没有看到有关此主题的任何综合材料。
书本上的情况并没有好转; 他们中的大多数人都专注于单元测试的基础知识,但除此之外并没有太多。 不要误会我的意思。 这些书有很多价值,尤其是当您刚开始进行单元测试时。 但是,学习并非以基础知识为结尾。 有一个新的水平:不仅是编写测试,而且以可以为您带来最大回报的方式进行单元测试。 当您达到这一点时,大多数书籍几乎都将您带到自己的设备上,以弄清楚如何进入下一个层次。
这本书带你到那里。 它教导了理想的单元测试的精确,科学的定义。 您将看到如何将此定义应用于实际的实际示例。 我希望这本书可以帮助您理解为什么尽管经过大量测试,您的特定项目还是可能偏偏,以及如何更好地纠正其过程。
如果您从事企业应用程序开发,您将从本书中获得最大的价值,但是核心思想适用于任何软件项目。
什么是企业应用程序?
企业应用程序是旨在自动化或辅助组织内部流程的应用程序。 它可以采用多种形式,但是通常企业软件的特征是
高业务逻辑复杂度
项目寿命长
适量的数据
中低性能要求
1.2. The goal of unit testing
在深入讨论单元测试之前,让我们先回顾一下,考虑一下单元测试帮助您实现的目标。人们常说单元测试实践能带来更好的设计。这是真的:为代码库编写单元测试的必要性通常会导致更好的设计。但这不是单元测试的主要目标;这只是一个令人愉快的副作用。
单元测试与代码设计之间的关系
对一段代码进行单元测试的能力是一个不错的石蕊测试,但是它只能在一个方向上工作。 这是一个很好的否定指标,它指出了质量相对较差的代码。 如果您发现很难对代码进行单元测试,则表明该代码需要改进。 质量低下通常表现为紧密耦合,这意味着不同的生产代码片段之间的耦合程度不够,因此很难单独测试它们。
不幸的是,对一段代码进行单元测试的能力是一个不好的肯定指标。 您可以轻松地对代码库进行单元测试的事实并不一定意味着它的质量很高。 即使项目表现出高度的去耦性,该项目也可能是一场灾难。
那么,单元测试的目标是什么? 目的是使软件项目可持续发展。 可持续这个词很关键。 扩展项目非常容易,尤其是当您从头开始时。 随着时间的推移,维持这种增长变得更加困难。
图1.1显示了未经测试的典型项目的增长动态。 您起步很快,因为没有什么可拖累您的。 尚未做出糟糕的架构决策,也没有任何现有代码可担心。 但是,随着时间的流逝,您必须投入越来越多的时间才能取得与开始时一样的进度。 最终,开发速度会大大降低,有时甚至达到您无法取得任何进展的地步。
快速降低开发速度的现象也称为软件熵。 熵(系统中的混乱程度)是一种数学和科学概念,也可以应用于软件系统。 (如果您对熵的数学和科学感兴趣,请查阅热力学第二定律。)
在软件中,熵以易于恶化的代码形式出现。 每当您在代码库中更改某些内容时,其中的混乱或熵就会增加。 如果不加以适当的护理(例如不断地进行清洁和重构),系统将变得越来越复杂和混乱。 修复一个错误会引入更多错误,而修改软件的一个部分则会破坏其他几个错误-就像多米诺骨牌效应。 最终,代码库变得不可靠。 最糟糕的是,很难使其恢复稳定。
测试有助于推翻这种趋势。 它们起着安全网的作用-一种为绝大多数回归提供保险的工具。 测试有助于确保现有功能正常运行,即使在您引入新功能或重构代码以更好地适应新要求之后也是如此。
回归的定义是在某个事件(通常是代码修改)之后,某个特性停止正常工作。术语回归和软件bug是同义词,可以互换使用。
这里的缺点是测试需要初始的,有时需要大量的工作。但从长远来看,他们会通过帮助项目在后期发展来为自己买单。如果没有不断验证代码库的测试的帮助,软件开发是无法扩展的。可持续性和可扩展性是关键。它们允许您在长期内保持开发速度。
1.2.1. What makes a good or bad test?
尽管单元测试有助于保持项目增长,但仅仅编写测试是不够的。写得很糟糕的测试仍然会导致同样的结果。如图1.2所示,糟糕的测试在一开始确实有助于减缓代码的恶化:与完全没有测试的情况相比,开发速度的下降不那么明显。但从大局来看,一切都没有改变。这样的项目进入停滞阶段可能需要更长的时间,但停滞仍然是不可避免的。
记住,不是所有的测试都是一样的。其中一些是有价值的,并且对整个软件质量做出了很大的贡献。其他的则不会。它们会发出错误警报,不会帮助您捕获回归错误,并且运行缓慢且难于维护。为了单元测试而编写单元测试,而不清楚它是否对项目有帮助,这很容易陷入这样的陷阱。仅仅通过在项目中进行更多的测试是不能达到单元测试的目的的。您需要同时考虑测试的价值和维护成本。成本部分是由花费在不同项目上的时间决定的
重构基础代码时重构测试
在每个代码更改上运行测试
处理测试引发的错误警报
尝试了解基础代码的行为时,请花时间阅读测试
由于维护成本高,很容易创建净值接近于零甚至为负的测试。为了使项目持续增长,您必须专注于高质量的测试——这是唯一值得保留在测试套件中的测试类型。
生产代码VS.测试代码
人们通常认为产品代码和测试代码是不同的。测试被假定为产品代码的附加物,没有所有权成本。推而广之,人们通常认为测试越多越好。但事实并非如此。代码是负债,不是资产。引入的代码越多,就越容易发现软件中的潜在bug,项目的维护成本也就越高。用尽可能少的代码解决问题总是更好的。
测试也是代码。您应该将它们视为代码库的一部分,目的是解决特定的问题:确保应用程序的正确性。单元测试,就像任何其他代码一样,也容易出现bug,需要维护。
了解如何区分好和坏单元测试至关重要。 我将在第4章介绍这个主题。
1.3. Using coverage metrics to measure test suite quality
在这一节中,我将讨论两个最流行的覆盖率指标代码覆盖率和分支覆盖率,如何计算它们,如何重用它们,以及它们存在的问题。我将说明为什么瞄准特定的覆盖率数字对程序员是有害的,以及为什么您不能仅仅依靠覆盖率指标来确定您的测试套件的质量。
定义
覆盖率指标显示测试套件执行的源代码数量,从无到100%。
覆盖率指标有多种类型,通常用于评估测试套件的质量。 普遍的信念是覆盖数越高越好。
不幸的是,它并不是那么简单,覆盖率指标虽然提供了宝贵的反馈,但不能有效地衡量测试套件的质量。 这与对代码进行单元测试的能力相同:覆盖率指标是一个很好的否定指标,但是是一个不好的肯定指标。
如果一项指标表明您的代码库覆盖率太低(例如只有10%),则表明您测试不足。 但是事实并非如此:即使100%的覆盖率也不能保证您拥有高质量的测试套件。 提供高覆盖率的测试套件的质量仍然很差。
我已经谈到了这样做的原因-您不能只对项目进行随机测试,希望这些测试能够改善情况。 但是,让我们针对代码覆盖率指标详细讨论这个问题。
1.3.1. Understanding the code coverage metric
第一个也是最常用的覆盖率指标是代码覆盖率,也称为测试覆盖率;参见图1.3。该指标显示了至少一个测试执行的代码行数与生产代码库中总代码行数的比率。
图1.3。代码覆盖率(测试覆盖率)指标计算为测试套件执行的代码行数与生产代码库中的总代码行数之间的比率。
让我们看一个示例,以更好地了解其工作原理。 清单1.1显示了一个IsStringLong方法以及涵盖该方法的测试。 该方法确定作为输入参数提供给它的字符串是否长(此处,long的定义是长度大于五个字符的任何字符串)。 该测试使用“ abc”来练习该方法,并检查该字符串是否不认为太长。
public static bool IsStringLong(string input)
{
if (input.Length > 5)
return true;
return false;
}
public void Test()
{
bool result = IsStringLong("abc");
Assert.Equal(false, result);
}
在此处轻松计算代码覆盖率。 该方法中的总行数为5(也包含大括号)。 测试执行的行数为四,测试通过除返回true以外的所有代码行; 声明。 这使我们获得4/5 = 0.8 = 80%的代码覆盖率。
现在,如果我重构该方法并像这样内联不必要的if语句怎么办?
public static bool IsStringLong(string input)
{
return input.Length > 5;
}
public void Test()
{
bool result = IsStringLong("abc");
Assert.Equal(false, result);
}
代码覆盖范围编号是否更改? 是的,它确实。 由于测试现在执行所有三行代码(return语句加上两个花括号),因此代码覆盖率增加到100%。
但是我通过这种重构改进了测试套件吗? 当然不是。 我只是重新整理了方法中的代码。 该测试仍会验证相同数量的可能结果。
这个简单的例子说明了如何计算覆盖率数字是多么容易。 您的代码越紧凑,测试覆盖率指标就越好,因为它只考虑原始行号。 同时,将更多的代码压缩到更少的空间不会(也不应该)改变测试套件的价值或基础代码库的可维护性。
1.3.2. Understanding the branch coverage metric
另一个覆盖率度量称为分支覆盖率。分支覆盖比代码覆盖提供更精确的结果,因为它有助于处理代码覆盖的缺点。这个指标不是使用原始代码行数,而是关注控制结构,如if和switch语句。它显示了套件中至少一个测试遍历了多少这样的控制结构,如图1.4所示。
图1.4。分支度量计算为测试套件执行的代码分支数量与生产代码库中的总分支数量的比率。
要计算分支覆盖率指标,您需要总结代码库中所有可能的分支,并查看测试访问了多少分支。 让我们再次以前面的示例为例:
public static bool IsStringLong(string input)
{
return input.Length > 5;
}
public void Test()
{
bool result = IsStringLong("abc");
Assert.Equal(false, result);
}
IsStringLong方法中有两个分支:一个用于string参数的长度大于五个字符的情况,另一个用于不存在的情况。 该测试仅覆盖这些分支之一,因此分支覆盖率指标为1/2 = 0.5 = 50%。 不管我们如何表示被测代码,无论我们是否像以前一样使用if语句还是使用较短的符号。 分支机构覆盖率指标仅考虑分支机构的数量; 它没有考虑实现这些分支需要花费多少行代码。
图1.5显示了一种可视化此指标的有用方法。 您可以将受测代码可以采用的所有可能路径表示为图表,并查看遍历了其中的多少。 IsStringLong有两条这样的路径,而测试仅执行其中一条。
图1.5。方法IsStringLong表示为可能的代码路径图。测试只覆盖了两个代码路径中的一个,因此提供了50%的分支覆盖率。
1.3.3. Problems with coverage metrics
覆盖率,您仍然不能依赖它们中的任何一个来确定您的测试套件的质量,原因有两个:
您不能保证测试验证了被测试系统的所有可能结果。
没有覆盖率指标可以考虑外部库中的代码路径。
让我们更仔细地看看这些原因。
您不能保证测试验证了所有可能的结果
对于要实际测试而不仅仅是执行的代码路径,您的单元测试必须有适当的断言。换句话说,您需要检查被测试的系统产生的结果是否是您期望它产生的确切结果。此外,这一结果可能有几个组成部分;为了使覆盖率指标有意义,您需要验证所有这些指标。
下一个清单显示了IsStringLong方法的另一个版本。它将最后一个结果记录到一个公共WasLastStringLong属性中。
public static bool WasLastStringLong { get; private set; }
public static bool IsStringLong(string input)
{
bool result = input.Length > 5;
WasLastStringLong = result;
return result;
}
public void Test()
{
bool result = IsStringLong("abc");
Assert.Equal(false, result);
}
IsStringLong方法现在有两个结果:一个显式结果,由返回值编码; 和一个隐式值,即属性的新值。 尽管不验证第二个隐式结果,但覆盖率指标仍将显示相同的结果:代码覆盖率100%,分支覆盖率50%。 如您所见,覆盖率指标不能保证底层代码已经过测试,只能保证它已在某些时候执行过。
这种情况的极端版本是经过部分测试的结果,这是无断言测试,这是当您编写不包含任何断言语句的测试时。 这是一个无断言测试的示例。
public void Test()
{
bool result1 = IsStringLong("abc");
bool result2 = IsStringLong("abcdef");
}
这个测试的代码和分支覆盖率指标都显示100%。但与此同时,它完全没用,因为它没有验证任何东西。
一个来自战壕的故事
无断言测试的概念可能看起来像一个愚蠢的想法,但它确实发生在野外。
几年前,我所从事的一个项目中,管理人员严格要求每个开发中的项目都有100%的代码覆盖率。这一倡议具有崇高的意图。那时候单元测试还不像现在这么流行。组织中很少有人实践它,更少人坚持进行单元测试。
一群开发人员参加了一个会议,会上有很多关于单元测试的讨论。回国后,他们决定把新知识付诸实践。高层管理人员支持他们,并开始向更好的编程技术转变。进行了内部演示。安装了新的工具。而且,更重要的是,一个新的全公司范围的规则被强加了:所有的开发团队必须专注于编写测试,直到他们达到100%的代码覆盖率标记。在他们达到这个目标之后,任何降低指标的代码签入都必须被构建系统拒绝。
正如你可能猜到的那样,结果并不好。被这一严重的限制所击垮,开发者开始寻找利用系统的方法。很自然,他们中的许多人都有同样的认识:如果您用try/catch块包装所有测试,并且不在其中引入任何断言,那么这些测试就可以保证通过。为了达到100%的强制覆盖率要求,人们开始盲目地创建测试。不用说,这些测试并没有给项目增加任何价值。此外,由于远离生产活动的所有努力和时间,以及维护测试前进所需的维护成本,它们破坏了项目。
最终,要求降低到90%,然后是80%;一段时间后,它被完全收回(为了更好!)
但是让我们假设您彻底地验证了测试代码的每个结果。这是否与分支覆盖率指标相结合,提供了一个可靠的机制,您可以使用它来确定您的测试套件的质量?不幸的是,没有。
没有覆盖率指标可以考虑外部库中的代码路径
所有覆盖率指标的第二个问题是,当被测试的系统在外部库上调用方法时,它们没有考虑外部库所经过的代码路径。让我们举个例子:
public static int Parse(string input)
{
return int.Parse(input);
}
public void Test()
{
int result = Parse("5");
Assert.Equal(5, result);
}
分支覆盖率指标显示为100%,测试验证方法结果的所有组件。它有一个这样的组件返回值。与此同时,这项测试还远远不够详尽。它没有考虑。net框架的int.Parse方法可能要经过的代码路径。而且,即使在这个简单的方法中,也有相当多的代码路径,如图1.6所示。
图1.6。外部库的隐藏代码路径。覆盖率度量没有办法看到它们有多少,以及您的测试中有多少。
内置整数类型具有大量分支,如果您更改方法的输入参数,这些分支将对测试隐藏,这可能会导致不同的结果。 以下是一些可能无法转换为整数的参数:
空值
空字符串
“不是int”
字符串太大
您可能会遇到许多极端情况,并且无法查看测试是否将所有情况都考虑在内。
这并不是说覆盖率指标应考虑外部库中的代码路径(不应),而是向您展示您不能依靠这些指标来查看单元测试的优劣。 覆盖率指标可能无法判断您的测试是否详尽无遗; 他们也不会说您是否有足够的测试。
1.3.4. Aiming at a particular coverage number
在这一点上,我希望您能够看到,仅仅依靠覆盖率度量来确定测试套件的质量是不够的。如果你开始将一个特定的覆盖率数字作为一个目标,它也可能导致危险的领域,它可能是100%,90%,甚至是适度的70%。查看覆盖率指标的最佳方式是作为一个指标,而不是其本身的目标。
想想医院里的病人。他们的高烧可能表明发烧,这是一个很有帮助的观察。但是医院不应该用任何必要的手段使病人的合适体温成为一个目标。否则,医院最终可能会采用一种快速而“有效”的解决方案,在病人旁边安装一台空调,通过调节流入皮肤的冷空气量来调节病人的体温。当然,这种方法没有任何意义。
同样地,瞄准一个特定的覆盖率数字会产生一个违背单元测试目标的错误动机。人们不再专注于测试重要的东西,而是开始寻找达到这个人为目标的方法。适当的单元测试已经够困难的了。强制的覆盖数字只会分散开发人员对测试内容的注意力,并且使适当的单元测试更难实现。
小贴士:在系统的核心部分有一个高水平的覆盖率是很好的。把这种高水平的要求作为一个要求是不好的。差别很微妙,但很关键。
让我重复一遍:覆盖率指标是一个很好的负面指标,但不是一个很好的正面指标。 覆盖率低(例如低于60%)肯定是麻烦的征兆。 它们意味着您的代码库中有很多未经测试的代码。 但是高数字并不意味着什么。 因此,测量代码覆盖率应该只是迈向质量测试套件的第一步。
1.4. What makes a successful test suite?
我在本章的大部分时间里都在讨论不正确的方法来衡量测试套件的质量:使用覆盖率指标。 正确的方法呢? 您应该如何衡量测试套件的质量? 唯一可靠的方法是分别评估套件中的每个测试。 当然,您不必一次评估所有这些; 这可能是一项巨大的工作,需要大量的前期工作。 您可以逐步执行此评估。 关键是无法自动查看测试套件的质量。 您必须运用个人判断。
让我们看一下使测试套件整体上成功的因素的更广阔的前景。 (我们将在第4章中深入介绍区分好测试和坏测试的细节。)成功的测试套件具有以下属性:
它已集成到开发周期中。
它仅针对代码库中最重要的部分。
它以最低的维护成本提供了最大的价值。
1.4.1. It’s integrated into the development cycle
进行自动化测试的唯一点是您是否经常使用它们。 所有测试都应集成到开发周期中。 理想情况下,您应该在每次代码更改(即使是最小的代码更改)上都执行它们。
1.4.2 它只针对代码库中最重要的部分
正如并非所有测试均创建一样,就单元测试而言,并非代码库的所有部分都值得同样关注。 测试提供的价值不仅在于这些测试本身的结构,还在于它们验证的代码。
重要的是将单元测试工作定向到系统的最关键部分,并仅短暂或间接地验证其他部分。 在大多数应用程序中,最重要的部分是包含业务逻辑的部分-域模型。[1] 测试业务逻辑可以为您带来最佳的时间投资回报。
1参见Eric Evans的《域驱动设计:解决软件核心中的复杂性》(Addison-Wesley,2003年)。
所有其他部分可以分为三类:
基础架构代码
外部服务和依赖关系,例如数据库和第三方系统
将所有内容粘合在一起的代码
但是,其中一些其他部分可能仍需要彻底的单元测试。 例如,基础结构代码可能包含复杂而重要的算法,因此也有必要进行大量测试来覆盖它们。 但通常,您的大部分精力都应该花在领域模型上。
您的某些测试(例如集成测试)可以超越域模型,并可以验证系统整体的工作方式,包括代码库的非关键部分。 很好。 但是重点应该放在领域模型上。
请注意,为了遵循该准则,您应该将域模型与代码库的非必要部分隔离开。 您必须将域模型与所有其他应用程序问题分开,这样才能将单元测试工作专门集中在该域模型上。 我们将在本书的第2部分中详细讨论所有这些内容。
1.4.3. It provides maximum value with minimum maintenance costs
单元测试中最困难的部分是以最低的维护成本获得最大的价值。 这是本书的重点。
仅将测试合并到构建系统中是不够的,还不足以维持对域模型的高测试覆盖率。 同样重要的是,只保留价值远远超过其维护成本的测试。
最后一个属性可以分为两部分:
认识到有价值的测试(进而扩展为低价值的测试)
编写有价值的测试
尽管这些技能看起来很相似,但本质上却有所不同。 要识别高价值的测试,您需要一个参考框架。 另一方面,编写有价值的测试需要您也了解代码设计技术。 单元测试和基础代码是高度交织的,如果不付出很大的精力来覆盖它们所涵盖的代码库,就不可能创建有价值的测试。
您可以将其视为识别一首好歌与能够创作一首歌之间的区别。 成为作曲家所需的精力不对称地大于区分好音乐和坏音乐所需的努力。 单元测试也是如此。 编写新测试比检查现有测试需要更多的工作,主要是因为您不是凭空编写测试:您必须考虑基础代码。 因此,尽管我专注于单元测试,但我也将本书的很大一部分用于讨论代码设计。
1.5 你会从这本书中学到什么
本书讲授了一个参考框架,可用于分析测试套件中的任何测试。 该参考框架是基础。 学习之后,您将能够以全新的方式查看许多测试,并查看其中哪些对项目有贡献,哪些必须重构或完全删除。
设置完这一阶段(第4章)后,本书将分析现有的单元测试技术和实践(第4-6章,以及第7章的一部分)。 您是否熟悉这些技术和做法都没关系。 如果您熟悉它们,将会从一个新的角度看到它们。 最有可能的是,您已经在直观上获得了它们。 这本书可以帮助您阐明为何一直使用的技术和最佳实践如此有用。
不要低估这项技能。 向同事清晰传达您的想法的能力是无价的。 如果软件开发人员(甚至是一个出色的软件开发人员)无法解释做出该决定的确切原因,则很少获得该决定的全部荣誉。 本书可以帮助您将知识从无意识的领域转变为您可以与任何人谈论的话题。
如果您对单元测试技术和最佳实践没有太多经验,那么您将学到很多东西。 除了可以用来分析测试套件中任何测试的参考框架外,该书还介绍了
如何重构测试套件及其涵盖的生产代码
如何应用不同样式的单元测试
使用集成测试来验证整个系统的行为
在单元测试中识别并避免反模式
除了单元测试外,本书还涵盖了自动化测试的整个主题,因此您还将了解集成和端到端测试。
我在代码示例中使用了C#和.NET,但是您不必一定是C#专业人员才能阅读本书; C#只是我碰巧最多使用的语言。 我所讨论的所有概念都是非语言特定的,可以应用于任何其他面向对象的语言,例如Java或C ++。
概要
代码趋于恶化。 每当您在代码库中更改某些内容时,其中的混乱或熵就会增加。 如果没有适当的护理(例如不断的清洁和重构),系统将变得越来越复杂且混乱。 测试有助于推翻这种趋势。 它们充当安全网-一种为绝大多数回归提供保险的工具。
编写单元测试很重要。 编写良好的单元测试同样重要。 带有不良测试或没有测试的项目的最终结果是相同的:每个新版本都停滞不前或大量回归。
单元测试的目的是使软件项目可持续发展。 一个好的单元测试套件有助于避免停滞阶段,并随着时间的推移保持开发进度。 有了这样的套件,您可以确信所做的更改不会导致回归。 反过来,这使得重构代码或添加新功能变得更加容易。
不是所有测试都相等。 每个测试都有一个成本和收益组成部分,您需要仔细权衡一个。 仅对套件中的正净值进行测试,并消除所有其他测试。 应用程序代码和测试代码都是负债,而不是资产。
对代码进行单元测试的能力是一个很好的石蕊测试,但是它仅在一个方向上起作用。 这是一个很好的否定指标(如果您无法对代码进行单元测试,则说明质量很差),但不好的肯定指标(对单元进行代码测试的能力并不能保证其质量)。
同样,覆盖率指标是一个好的负面指标,但不是一个好的正面指标。 覆盖率低表明一定会出现问题,但是覆盖率高并不意味着您的测试套件质量高。
分支机构的覆盖范围可让您更好地了解测试套件的完整性,但仍无法表明套件是否足够好。 它没有考虑断言的存在,也没有考虑代码库使用的第三方库中的代码路径。
强加一个特定的承保范围编号会产生有害的激励作用。 在系统的核心部分具有较高的覆盖率是件好事,但将其设为必需品是件坏事。
成功的测试套件具有以下属性:
它已集成到开发周期中。
它仅针对代码库中最重要的部分。
它以最低的维护成本提供了最大的价值。
实现单元测试目标(即实现可持续项目增长)的唯一方法是
了解如何区分好测试和坏测试。
能够重构测试以使其更具价值。