关于单元测试体系结构的一些心得

自动化测试是任何大型软件项目不可或缺的一部分,可作为提高质量,生产率和灵活性的一种手段。因此,至关重要的是,系统架构的设计必须能够促进自动化测试的开发和执行。

质量得到提高,因为自动化测试的执行可以让我们找到,并在开发周期的早期解决问题,很多之前产品变更部署到生产和可用给最终用户。

生产率提高是因为在开发周期中发现问题的时间越早,修复该问题的成本越低,并且不难理解为什么。如果软件开发人员能够在将代码更改集成到主存储库之前运行自动化测试套件,则他可以快速发现新引入的错误并将其修复。但是,如果没有这样的测试套件,则新引入的错误可能只会在以后由最终用户报告的手动测试阶段中出现,甚至更糟,这要求开发人员退出常规开发工作流程以进行调查和修复。

灵活性得到了改善,因为开发人员在依赖于测试覆盖率较高的测试套件来评估其代码更改的影响时,对重构代码,升级程序包以及在需要时修改系统行为更有信心。

在讨论自动化测试时,我也喜欢将风险管理的话题引入对话中。作为首席软件工程师,风险管理是我工作的重要组成部分,它涉及对开发团队进行实践和流程指导,以减少产品技术退化的风险。从上面列出的好处中可以明显看出,采用适当的自动化测试策略很合适,可以帮助减轻软件项目中的风险。

展望未来,我们可以根据实现和运行自动测试的策略将自动测试分为至少三种不同类型,如以下著名的测试金字塔所示:

关于单元测试体系结构的一些心得

就使用的时间和资源而言,单元测试的开发成本低且运行成本低,并且单元测试专注于测试与外部依赖项隔离的单个系统组件(例如,业务逻辑)。

集成测试向前迈了一步,并且在不隔离外部依赖关系的情况下进行了开发和运行。在这种情况下,我们有兴趣评估所有系统组件在组合在一起并面临集成约束(例如:联网,存储,处理等)时是否按预期进行交互。

最后,在金字塔的顶端,图形用户界面测试是自动化和执行最昂贵的。他们通常依靠UI输入/输出脚本和回放工具来模仿最终用户与系统图形用户界面的交互。

在本文中,我们将重点介绍测试金字塔的基础,单元测试以及促进采用它们的系统体系结构注意事项。

有效单元测试的属性

让我们列举一下什么是有效的,精心设计的单元测试。以下是一个命题:

简短,只有一个目的

简单,清晰的设置和拆卸

快速,只需几分之一秒即可执行

标准化,遵循严格的约定

理想情况下,单元测试应显示所有这些属性,下面我详细说明原因。

如果单元测试不够短,将很难阅读并理解其目的,即确切地说是测试的内容。因此,出于这个原因,单元测试应该有一个明确的目标,并且只评估一件事,而不是尝试同时执行多个评估。这样,当单元测试失败时,开发人员将更加轻松快捷地评估情况并进行修复。

如果单元测试需要大量的精力来设置他们的测试环境,然后将其拆除,则开发人员通常会开始质疑,花费在编写这些测试上的时间是否值得。因此,我们需要提供一个编写单元测试的环境,该环境要管理测试上下文的所有复杂性,例如注入依赖关系,预加载数据,清除缓存等。编写单元测试越容易,开发人员创建它们的动力就越大!

如果执行一组单元测试要花费大量时间,则开发人员自然会减少执行频率。这里的危险在于拥有如此冗长的单元测试套件,以至于变得不切实际,开发人员开始跳过运行它或有选择地运行它,从而降低了其有效性。

最后,如果测试未标准化,不久后您的测试套件将开始看起来像狂野的西部,编写单元测试所使用的编码风格有时会有所不同,有时会发生冲突。因此,在整个单元测试范围内,追求系统设计的一致性对于整个系统来说同样有效。

一旦我们对有效的单元测试的构成达成共识,就可以开始定义提升其性能的系统架构准则,如以下各节所述。

软件复杂度

除其他因素外,软件复杂性还源于系统中组件之间不断增长的交互次数以及内部状态的演变。随着复杂度的提高,无意识地干扰复杂的组件交互网络的风险也随之增加,有可能导致在更改代码时引入缺陷。

此外,根据常识,系统的复杂性越高,维护和测试它就越困难,这导致了第一个(一般)准则:

密切关注软件的复杂性并遵循设计实践来控制它

在管理复杂性同时提高可测试性时,值得一提的做法是在系统设计中尽可能采用“ 纯函数”和“ 不变性”。纯函数是具有以下属性的函数:1

对于相同的参数,其返回值是相同的(局部静态变量,非局部变量,可变参考变量或来自I / O设备的输入流无变化)。

它的评估没有副作用(本地静态变量,非本地变量,可变引用参数或I / O流不会发生突变)。

从其特性可以明显看出,纯函数非常适合于单元测试。它们的用法也消除了对许多补充性实践的需求,这些补充性实践将在以下各节中讨论,以处理大多数有状态的组件。

不变性同样重要。不可变对象是创建后状态无法更改的对象。它们更易于交互且更可预测,从而有助于降低系统复杂性,消除全局状态。

隔离依赖

按照它们的定义,单元测试旨在隔离地测试各个系统组件,因为我们不希望组件的单元测试的结果受到其依赖项之一的影响。隔离程度会根据被测组件的具体情况以及每个开发团队的偏好而有所不同。我个人不担心隔离轻量级的内部业务类,因为我发现用测试目标组件替代它们并没有增加任何价值,该组件将显示几乎相同的行为。尽管如此,这里的策略可能很简单:

在组件设计中应用依赖项反转模式

依赖关系反转模式(DIP)指出,高级对象和低级对象都应依赖抽象(例如接口),而不是特定的具体实现。一旦将系统组件从其依赖关系中分离出来,我们就可以在单元测试的上下文中通过简化的,针对测试的具体实现轻松地替换它们。下面的类图说明了结果结构:

关于单元测试体系结构的一些心得

在此示例中,被测组件依赖于资料库和文件存储抽象。当部署到生产环境中时,我们可能会为存储库类注入基于SQL的具体实现,并为文件存储组件注入基于S3的实现,以便在AWS Cloud中远程存储文件。但是,在运行单元测试时,我们将希望注入不依赖外部服务的简化功能实现,例如以绿色绘制的“内存中”实现。

如果您不熟悉DIP,那么我还会发表另一篇文章,内容是有关如何在可能会有所帮助的相似上下文中使用DIP的实用概述:集成第三方模块。

假装与假货辩论

请注意,我并不是将这些“内存中”实现称为“模仿”,它们是模拟对象,它们以有限的受控方式模仿了真实对象的行为。我故意这样做,因为我反对使用模拟对象,而建议使用完全兼容的“伪”实现,这为我们提供了编写单元测试的更大灵活性,并且可以比设置更可靠的方式在多个单元测试类中重用模拟。

为了更详细地说明,假设我们正在编写一个依赖于组件的单元测试。 文件存储抽象。在此测试中,组件将一个项目添加到文件存储中,但实际上并不担心操作是成功还是失败(例如,日志文件),因此,我们决定以“虚拟”方式模拟该操作。现在,假设稍后需求发生变化,并且组件需要确保在继续操作之前通过从文件存储中读取文件来创建文件,这迫使我们更新模拟的行为以使测试通过。然后,想象需求再次发生变化,并且组件需要写入多个文件(例如:每个日志级别一个),而不是仅写入一个,从而迫使我们的模拟对象行为得到另一种改善。你知道发生了什么吗?我们正在逐步改进我们的模拟,使其更类似于具体的实现。更糟糕的是,我们最终可能会遇到许多独立的,半生不熟的,

为了解决这种情况,我提出以下准则:

依靠Fakes而不是Mocks来实施单元测试,将它们视为一流的公民,并将其组织为可重用的模块

由于Fake组件实现了商业行为,因此与设置模拟相比,它们本质上是更昂贵的初始投资。但是,它们的长期回报肯定更高,并且更符合有效的单元测试的特性。

编码风格

每个自动化测试都可以描述为三步脚本:

1、准备测试环境

2、执行按键操作

3、验证结果


这是合乎逻辑的考虑,给出一个初始已知状态,当执行一个操作,那么就应该产生相同的预期结果,每一次。为了使结果变得不同,必须更改初始状态,或者更改操作实现本身。

您可能对上面用黑体字标出的单词很熟悉。如果不是这样,它们代表了一种流行的Given-When-Then模式,以一种有利于可读性和结构的方式编写单元测试。这里的想法很简单:

为编写单元测试定义并实施单一的标准化编码样式

可以使用多种方式采用“当时给定”模式。其中之一是将单元测试方法构造为三种不同的方法。例如,考虑密码强度测试:

关于单元测试体系结构的一些心得

使用这种方法,主要的测试方法变成了对单元测试目的的三行描述,即使是非开发人员也可以通过阅读它轻松地理解。在实践中,单元测试的主要方法最终成为系统行为的低级文档,不仅提供文本描述,还提供执行代码,调试代码并找出内部情况的可能性。当新开发人员加入团队时,这对于缩短系统架构学习曲线非常有价值。

重要的是要强调,在编码风格方面,没有唯一正确的方法。我在上面提供的示例可能会使某些开发人员感到不满,例如,因为冗长而令人不悦,这没关系。真正重要的是,在您的开发团队中就编码惯例达成协议,以编写对您有意义并坚持下去的单元测试。

管理测试环境

单元测试上下文管理是一个讨论不够多的主题。“测试上下文”是指成功运行单元测试所需的整个依赖项注入和初始状态设置。

如前所述,当开发人员花费较少的时间来担心设置测试上下文并花更多时间编写测试用例时,单元测试会更有效。我们从以下观察得出我们的最后一个准则,即大量测试案例可以共享一些测试上下文:

利用构建器类将测试上下文的构建与单元测试用例的实现分开

这个想法是将测试上下文的构造逻辑封装在构建器类中,并在单元测试类中引用它们。然后,每个上下文构建器负责创建特定的测试方案,并可选地定义用于使其特定化的方法。

让我们看一下另一个说明性的代码示例。假设我们正在开发一个反欺诈组件,用于检测移动应用程序用户的可疑位置变化。测试上下文生成器可能如下所示:

关于单元测试体系结构的一些心得

关于单元测试体系结构的一些心得

由此创建的测试上下文MobileUserContextBuilder足够通用,因此从应用程序已经注册了移动用户的状态开始所需的任何测试用例都可以使用它。最重要的是,它定义了AddDevice具体化测试环境以适应我们虚拟的反欺诈组件测试需求的方法。

考虑调用此反欺诈组件GeolocationScreener,它负责检查移动用户的位置是否更改得太快,这表明他可能是在伪造自己的真实坐标。其单元测试之一可能如下所示:

关于单元测试体系结构的一些心得

关于单元测试体系结构的一些心得

可见,在此示例测试类中专用于设置测试上下文的代码量很小,因为它几乎完全包含在builder类中,从而保留了代码的可读性和组织性。随着越来越多的测试用例利用可用的测试上下文构建器库,设置测试上下文所需的摊销时间变得非常短。

结论

在本文中,我讨论了单元测试的主题,提供了五个主要指南,以应对在不断增长的测试用例中保持有效性的挑战。这些准则对系统体系结构有重要影响,从软件项目开始就应该考虑单元测试要求,以促进开发人员在其中看到价值并有动力编写单元测试的环境。

单元测试应被视为系统体系结构的组成部分,与它们所测试的组件一样重要,而不应被视为开发团队仅出于填写管理报告复选框或提供指标而编写的二等公民。

最后,如果您正在使用很少或没有单元测试的遗留项目中,而没有使用DIP,则由于您有意避免谈论复杂的模拟框架,因此本篇文章可能没有为您提供最佳策略。遗留项目的上下文成为将单元测试引入极度耦合的代码的可行选择。



分享到:


相關文章: