使用 DrillSergeant 进行行为测试
作者:互联网
介绍
行为测试是软件开发中经常被忽视的一部分。公司通常擅长推广基于 TDD 的软件开发,许多公司利用 SonarQube 等工具作为 CI/CD 管道中的质量门,以确保代码具有足够的测试覆盖率。
不幸的是,当涉及到行为测试时,事情往往会变得有点模糊。在大多数情况下,在开发某个功能时,只要开发人员能够证明该功能满足其用户故事的验收标准(通常通过向利益相关者进行现场演示)并且代码满足 CI/CD 要求,该功能就会获得批准,代码也会得到批准被合并。但这是一个相当糟糕的策略,因为一旦代码被合并,利益相关者就失去了该功能是否会继续工作的所有可见性。通常,即使没有测试实际失败,代码库其他部分的行为更改也可能会破坏功能。这可能会导致不必要的故障排除,然后进行更多测试或对本应失败的现有测试进行更改。
与旨在单独测试代码块的正确性的单元测试不同,行为测试感兴趣的是测试多个组件协同工作以产生预期结果的行为。例如,假设我们必须实现登录网站的功能。这有几个不同的组成部分:
- 我们需要编写一个验证用户输入的表单。
- 我们需要编写可以与数据库通信并读取用户密码的代码。
- 我们需要编写代码来根据系统中注册的用户列表检查用户密码。
- 当用户访问网站的受限部分时,我们需要生成某种令牌来授权用户。
这显然不是一个详尽的列表,但正如您所看到的,即使是简单的用户任务也通常需要许多单独的组件一起工作。单独测试每个组件显然很重要,但仅仅因为各个部分工作并不一定意味着该功能本身工作。
.Net 开发人员常用的 BDD 工具
有一些工具可用于协助行为测试。它们是:FitNesse、SpecFlow和Gauge。每个都使用自己的风格来编写测试(例如,分别是 wiki、gherkin、markdown),并具有某种将规范与实际代码联系起来的机制。
虽然上述工具肯定能够满足大多数应用中 BDD 测试的要求,但它们都面临着类似的挑战:主要是它们编写测试规范的模型与开发人员编写实现代码的方式之间存在相当大的阻抗不匹配。 。
示例:SpecFlow
由于与 Visual Studio 的紧密集成,SpecFlow 可以说拥有最佳的开发人员体验。它作为扩展安装,允许开发人员在 IDE 中编写功能文件。它还使用库配置为使用的任何测试框架 (xunit/nunit/msbuild) 自动将功能文件转换为 C# 测试。这使得您可以轻松地以最小的努力起步并跑步。也就是说,安装扩展后,开发人员仍然需要采取许多步骤来创建行为测试:
- 在 Visual Studio 中创建一个新的功能文件。
- 为场景编写 Gherkin 步骤(SpecFlow 组合中的“行为”)。
- 创建一个单独的 C# 类来实现步骤代码。
- 每个步骤方法都需要包含一个属性,表示它是哪种类型的步骤(
Given
//When
)Then
,该属性包含用于将方法与特征文件中的步骤进行匹配的正则表达式。 - (可选)为步骤定义标签以避免全局步骤污染。尽早做到这一点至关重要。
除此之外,开发人员还需要熟悉 Gherkin 语法,即在项目编译之前转换为 C# 单元测试的 DSL。
以下是使用 SpecFlow 测试通用计算器类的示例。首先我们有用 Gherkin 语法编写的功能文件。
功能:计算器 @calculator场景 :将两个数字相加给定我有 数字<a>和<b> ,当这两个数字相加时, 结果应该是<预期>示例:| 一个| 乙| 预计| | 1 | 2 | 3 | | 2 | 3 | Gherkin 是一种流行的编写行为测试的格式。在 Gherkin 中,步骤被定义为使用“Given/When/Then”格式描述应该发生的事情的英语句子。这类似于单元测试中通常使用的常见 Arrange/Act/Assert 约定。
DrillSergeant 支持 GWT 和 AAA 风格的测试。
接下来是绑定类:
[Binding, Scope(Tag = "calculator")]Binding, Scope(Tag = "calculator" ) ] public seal class CalculatorStepDefinitions { private readonly ScenarioContext _context; 公共 CalculatorStepDefinitions ( ScenarioContext context ) { _context = context; } [ Give(@ "我有数字 (\d+) 和 (\d+)" ) ] public void GiveTwoNumbers ( int a, int b ) { _context[ "a" ] = a; _上下文[ “b”] = b; } [ When( "两个数字相加" ) ] public void WhenTheTwoNumbersAreAdded () { int a = ( int )_context[ "a" ]; int b = ( int )_context[ "b" ]; var计算器 = new Calculator(); _context[ "结果" ] =计算器.Add(a, b); } [ then(@ "结果应该是 (\d+)" ) ] public void TheResultShouldBe ( int预期) { int结果 = ( int )_context[ "结果" ]; Assert.Equal(预期,结果); } }
绑定类是标有[Binding]
属性的常规类,以便 SpecFlow 知道在其中查找步骤定义。在此示例中,我们还使用[Scope]
属性对其进行了标记,以限制可以使用它的场景。方法本身标有动词属性,SpecFlow 提供一个ScenarioContext
对象以安全的方式缓存步骤之间的数据。
虽然所有这些都相当简单,但设置起来需要做的工作比实际应该做的要多得多。开发人员需要跟踪用 DSL (Gherkin) 编写的代码、它们如何与步骤方法(魔术字符串)对齐以及它们如何组织(范围和标签),以在步骤数量增长时保持理智。确实,SpecFlow 与 Visual Studio 的集成可以实现部分自动化,但这更像是一种绷带,而不是真正的治愈方法。
让我们看看如何更好地简化这个过程。
光谱的另一端:XBehave
xbehave是对 DrillSergeant 产生重大影响的一个库。它使用一个非常聪明的技巧来连接到 xunit 管道,并将常规 xunit 测试方法转换为成熟的行为测试,每个步骤都是它们自己的测试用例。如果上面的计算器示例在 xbehave 中重写,它可能如下所示:
[场景]场景] [示例(1, 2, 3) ] [示例(2, 3, 5) ] public void Addition ( int x, int y, int Expected, Calculator Calculator, int Answer ) { "给定一个计算器" .x( () => 计算器 = new ()); “当我将数字加在一起时” .x(() => answer =计算器.Add(x, y)); "那么答案是 3" .x(() => Assert.Equal( 3 , answer)); }
这确实简单了一些!Xbehave 与 xunit 的紧密集成使开发人员可以立即开始编写行为测试,而无需任何样板,这对于快速原型设计和整体开发人员体验非常有用。
在 xbehave 中,开发人员所需要做的就是创建一个字符串并调用x()
它的扩展方法来表示这是在行为中执行的步骤。系统将首先执行周围的方法以收集所有步骤,然后按顺序执行每个步骤。该系统的另一个重要特征是它与所使用的措辞无关。使用 GWT、AAA 或任何所需的语言模式编写测试很容易。
然而,它的不足之处在于缺乏对其他 BDD 框架支持的许多功能的支持(例如,可重用步骤、上下文管理、DI 等)。
教官介绍
DrillSergeant 的目标是让像 xbehave 一样完全用 C# 轻松编写行为测试,但同时保留 Gherkin 的丰富语法以及更成熟的工具提供的功能。通过减少编写行为测试时的摩擦,开发人员将更加渴望编写它们,这将导致更全面的测试,并最终带来更可靠的软件。
已经显示代码
现在让我们看看如何在 DrillSergeant 中编写我们的计算器示例?一种方法可能如下所示:
[行为]行为] [ InlineData(1, 2, 3) ] [ InlineData(2, 3, 5) ] 公共行为加法( int a, int b, int Expected ) { var Calculator = new Calculator() var input = new { A = a, B = b, 预期 = 预期 }; return new Behaviour(input) .Given( "缓存数字" , CacheNumbers) .When(AddNumbers(calculator)) .Then<CheckResultStep>(); }
乍一看,这可能看起来与 xbehave 版本相似,但有一些细微的差别。
- 该行为返回一个实例
Behavior
(或者Task<Behavior>
对于异步)。与 xbehave 不同,xbehave 执行方法并收集所有对x()
DrillSergeant 的调用,DrillSergeant 行为被流畅地定义并返回到 xunit 来执行。这使得配置行为的运行方式变得更加容易。 - 可以将可选输入传递给行为的构造函数。此输入是不可变的,并且可供任何需要访问它的步骤使用。
- 行为测试有与之相关的背景。在内部,它们存储为
IDictionary<string,object?>
并使用该dynamic
类型动态访问。每个步骤都可以访问上下文并且可以自由修改它。 - 对行为的 GWT 调用链接在一起,而不是对 的稀疏调用
x()
。这迫使开发人员将行为视为一系列步骤,而不是要调用的函数中的方法。 - 与 xbehave 不同,步骤不限于简单的功能。DrillSergeant 中可以定义三种基本类型的步骤(更多内容见下文)。
- 名称是可选的。如果未提供,则 DrillSergeant 将根据传入的步骤对象自动派生它。
步骤到底是什么?
步骤是在高层次上描述应该采取的操作的对象。所有步骤都有一个关联的动词(例如,Given/When/Then)、描述性名称和处理程序。步骤共有三种常见类型:函数、Lambda 和类。让我们依次看一下。
私有 无效 CacheNumbers(动态上下文,动态输入) { context.A = input.A; 上下文.B = 输入.B; }
这是常规功能步骤。函数步骤最多可以采用两个参数:context
、 和input
。这些最适合短期一次性使用,因为它们很难重复使用。
默认情况下,DrillSergeant 会将所有输入和上下文视为dynamic
. 但是,所有链接方法都具有通用重写,以允许开发人员选择输入安全参数。
公共 类 MyContext { 公共 int A {获取; 设置;} 公共 int B {得到; 设置;} } private void CacheNumbers ( MyContext context,动态输入) { context.A = input.A; 上下文.B = 输入.B; }
这里我们为我们的context
. 这为我们提供了类型安全和智能感知支持。也可以这样做input
,或者(两者)
在对上下文或输入使用泛型时需要注意的一件重要事情是,DrillSergeant 不会将其内部内容强制转换为类型。相反,它使用反射来映射它。
继续下一类型的步骤是 lambda 步骤。
private LambdaStep AddNumbers () => new LambdaStep( "添加数字" ) .Handle(context => { context.Result = Calculator.Add(context.A, context.B); });
这与常规函数类似,但有一个显着差异。与函数步骤不同,这是通过调用另一个函数返回的函数。这使其成为一个高阶函数。这样做可以让我们参数化步骤,而不必将它们缓存在上下文中。
提示:幕后函数步骤只是用于创建 lambda 步骤的包装器。
最后是课堂步骤。
公共 类 CheckResultStep : ThenStep { 公共 void Then (动态上下文,动态输入) { Assert.Equal(input.Expected, context.Result); } } }
与前两步不同。班级步骤有一些独特的特征。这里没有virtual
处理程序override
。DrillSergeant 将依赖约定来选择要执行的处理程序。由于此步骤源自类ThenStep
,因此动词自动设置为“Then”。执行此步骤时,执行器将扫描类型以查找任何名为Then
或 的方法ThenAsync
,并选择参数最多的方法。
注意:如果找到两个参数数量相同且其中一个为 的处理程序
async
,则默认获胜。
将所有内容放在一起,整个示例如下所示:
[行为]行为] [ InlineData(1, 2, 3) ] [ InlineData(2, 3, 5) ] 公共行为加法( int a, int b, int Expected ) { var Calculator = new Calculator() var input = new { A = a, B = b, 预期 = 预期 }; return new Behaviour(input) .Given( "缓存数字" , CacheNumbers) .When(AddNumbers(calculator)) .Then<CheckResultStep>(); } private void CacheNumbers (动态上下文,动态输入) { context.A = input.A; 上下文.B = 输入.B; } private LambdaStep AddNumbers () => new LambdaStep( "添加数字" ) .Handle(context => { context.Result = Calculator.Add(context.A, context.B); }); 公共 类 CheckResultStep:ThenStep { 公共 无效 Then(动态上下文,动态输入) { Assert.Equal(input.Expected, context.Result); } }
这有点冗长,但这只是因为我们展示了不同的步骤类型。让我们利用 lambda 函数和闭包来清理它,以避免不必要的缓存。
[行为]行为] [ InlineData(1, 2, 3) ] [ InlineData(2, 3, 5) ] 公共行为加法( int a, int b, int Expected ) { varCalculator = new Calculator() return new Behaviour() .When (AddNumbers(计算器, a, b)) .Then(CheckResult(预期)); } private LambdaStep AddNumbers (计算器计算器, int a, int b ) => newLambdaStep( $"将数字{a}和{b}相加" ) .Handle(context => { context.Result = Calculator.Add(a,b); }); private LambdaStep CheckResult ( int Expected ) => new LambdaStep( $"检查结果匹配{expected} " ) .Handle(context => { Assert.Equal(expected, context.Result); });
首先应该跳出来的是,不再有任何Given
步骤了。那是因为没有必要。Lambda 步骤可以捕获传递给它们的参数,因此我们只需直接将参数传递给它们即可。
接下来是这两个步骤现在可以重复使用。虽然AddNumbers
测试其他行为可能不需要该步骤,但CheckResult
很可能需要该步骤。
此外,这些步骤会自动以一种对开发人员来说自然直观的方式确定范围。只有该要素类中的行为才能访问这些步骤。想让步骤更具体吗?创建一个子类并将其推入继承树。更开放?创建一个基类并将其拉起。如果需要全局化怎么办?创建一个静态类并将其公开为public static
.
最后,作为免费的奖励,因为我们可以访问步骤中的参数,所以我们甚至可以定制步骤名称以反映输入。
安装
DrillSergeant 是一个常规库,可以通过 nuget 包管理器或通过命令行安装。
# Nuget CLI Install-Package DrillSergeant -Version 0.1.0-beta # Dotnet CLI dotnet add package DrillSergeant --version 0.1.0-beta
注意:在撰写本文时,DrillSergeant 被视为测试版,因此在 Visual Studio 中通过 NuGet 包管理器安装时必须选中“包括预发布”复选框。
运行测试
因为 DrillSergeant 是建立在 xunit 之上的,所以不需要特殊的步骤来测试您的行为。它们可以与常规单元测试一起执行,或者使用命令通过命令行执行dotnet run test
。在 Visual Studio 中运行并检查测试资源管理器后,输出应如下所示:
除了打印测试的常规输出之外,DrillSergeant 还将记录传递给行为的输入以及有关所执行的每个步骤的信息。请注意,显示的输入是传递到行为本身的输入。不是测试方法的参数。
详细步骤信息
默认情况下,上下文日志记录处于关闭状态。要打开它,只需EnableContextLogging()
在创建行为时在调用链中的某个位置添加一个调用即可。测试时输出将如下所示:
这对于跟踪步骤之间上下文的状态如何受到影响特别有帮助。
过滤测试
有时,在积极开发时,将行为测试与单元测试一起运行效率不高。Xunit 以[Trait]
属性的形式提供了一种简单的机制。特征允许定义键/值对并将其与测试关联。像测试资源管理器这样的工具可以让开发人员根据此进行过滤。由于行为测试只是常规的 xunit 测试,因此该技术将继续发挥作用。然而,为了让事情变得更容易,该[Behavior]
属性也派生自ITraitAttribute
so 而不必编写
[Trait("功能", "计算器测试")]Trait( "功能" , "计算器测试" ) ] [行为] 公共行为AdditionTests () { ... }
你可以这样写:
[Behavior(Feature="Calculator Tests")]行为(功能= “计算器测试”) ] 公共行为AdditionTests() {...}
这两者将产生完全相同的效果。
概括
希望这足以让您对探索编写行为测试感兴趣。DrillSergeant 允许您完全用 C# 编写行为测试,而无需使用任何外部工具(例如,gauge)或 Visual Studio 扩展(例如,SpecFlow)。
该库仍然相对较新,因此如果您发现任何错误,请告诉我。如果您喜欢它,请务必给它一颗星星!
标签:单元测试框架,DrillSergeant,Xunit 来源: