单元测试 - BDD、AAA 结构和对象模拟






4.73/5 (12投票s)
单元测试中的 BDD、AAA 结构和对象模拟
单元测试
测试方法的命名约定
单元测试的传统原则
一个测试方法只测试一个方法;一个 Assert
方法一次只测试一个期望。
简而言之,原则是——“每个测试方法一个函数/方法和一个断言”
那么,我们来看下面的例子
将传统原则与现实世界进行比较
测试场景
验证“GetSum
”方法。
测试用例
积极测试用例
TC1
:给定正值,应返回预期结果测试数据-1
:firstValue =5
,secondValue =6
消极测试用例
TC2
:给定零值,应生成无效参数消息测试数据-2
:firstValue =0
,secondValue =0
TC3
:给定负值,应生成无效参数消息测试数据-3
:firstValue =-5
,secondValue =-6
异常测试用例
TC4
:给定阈值,应抛出异常消息测试数据-4
:firstValue =2147483647
,secondValue =2147483647
测试方法示例
现在,根据传统原则,我们来编写“GetSum
”的测试方法。
现在,根据传统原则,我们已经用“Test Data-1
”覆盖了积极测试用例。但是消极测试用例和异常测试用例呢?
我们如何用传统原则覆盖消极测试用例和异常测试用例?
行为驱动开发 (BDD)
为什么我们需要 BDD
如果我们想根据前面的例子覆盖所有测试用例的行为,那么我们需要遵循一些技术;这样,我们就可以写下该方法的所有行为。因此,BDD 作为一种技术,为我们提供了用标准和可读的命名约定来完成所有测试用例的机会。千人千面。有很多方法可以编写测试方法的命名约定。但这确实只取决于你和你的偏好。如果你遵循其他不同的技术,没有什么对错之分。总之,我们可以说——在 BDD 中,组件测试它们的预期行为。
BDD 的概念
- 鉴于我是 BDD 技术的新手,我以前从未使用过这项技术
- 当我阅读这篇关于 BDD 的教程时
- 那么我开始喜欢它,最后我学会了它
BDD 命名约定
测试场景 - 验证“GetSum
”方法
测试用例 - 积极测试用例
TC1
:给定正值,应返回预期结果测试数据-1
:firstValue =5
,secondValue =6
测试方法 - 命名约定
鉴于PositiveVaidValuesAsParams_当GetSumIsCalled_那么ItShouldReturnSumValue
更易读
鉴于_Positive_Vaid_Values_As_Params_当_GetSum_Is_Called_那么_It_Should_Return_Sum_Value
消极测试用例
TC2
:给定零值,应生成无效参数消息测试数据-2
:firstValue =0
,secondValue =0
测试方法 - 命名约定
鉴于ZeroValuesAsParams_当GetSumIsCalled_那么ItShouldThrowInvalidArgumentException
更易读
当_给定_零_值_作为_参数_时_调用_GetSum_则_它_应该_抛出_无效_参数_异常
TC3
:给定负值,应生成无效参数消息测试数据-3
:firstValue =-5
,secondValue =-6
测试方法 - 命名约定
鉴于NegativeValues_当GetSumIsCalled_那么ItShouldThrowInvalidArgumentException
更易读
鉴于_Negative_Values_当_GetSum_Is_Called_那么_It_Should_Throw_Invalid_Argument_Exception
异常测试用例
TC4
:给定阈值,应抛出异常消息测试数据-4
:firstValue =2147483647
,secondValue =2147483647
鉴于MaxLimitValuesOfIntAsParams_当GetSumIsCalled_那么ItShouldThrowSumException
更易读
当_给定_Int_的最大_限制_值_作为_参数_时_调用_GetSum_则_它_应该_抛出_求和_异常
测试方法的正文结构
对此没有硬性规定。我们通常遵循 AAA 结构,因为它易于阅读和理解。Test
方法的通用 AAA 结构如下。
AAA - 结构
- Arrange
- Act
- 断言(Assert)
Arrange
在此 Arrange 部分中,我们声明变量并创建类的对象实例。
Act
验证方法在该部分中被调用。这主要用于将输入参数传递给方法并从调用方法中收集实际返回结果。
断言(Assert)
在此部分中,我们将调用方法的实际输出结果与预期输出结果进行比较。测试的通过或失败取决于此 Assert
部分。
用于单元测试的对象模拟
为什么我们需要模拟
假设您需要测试某个方法的行为,并且它内部有一个外部服务或方法。在单元测试期间,我们必须避免这些外部依赖,而模拟技术使我们能够避免外部方法或服务的行为测试。
为什么?因为,根据第一个原则,我们都知道——“只测试类的逻辑,别无其他”单元测试从不使用——配置设置、数据库、日志、另一个应用程序/服务/文件/网络 I/O。
无论如何,我们来看这个例子
这个 GetSum
方法内部没有外部依赖。因此,它不需要对象模拟。
现在,看下面类的例子,它有两个依赖项,分别是 CheckingAccount
和 SavingAccount
。
现在,我们想验证方法“GetTotalMoneyByUserAccount
”,它内部有两个依赖项。因此,为了解决这种情况,我们需要对象模拟。
模拟框架
有很多对象模拟框架,如 Typemock Isolator、Rhino Mocks、Moq 和 NMock。您可以使用其中任何一个。
模拟前的要求
- 该类不能是密封的。
- 该方法不能是
static
;但如果需要,请使用适配器模式。 - 您可以模拟具体类上的接口、抽象方法或属性或虚方法或属性。
为单元测试项目添加 MOQ
选择您的单元测试项目。转到引用> 右键单击鼠标并选择“管理 NuGet 包”。现在在搜索文本框中输入“Moq
”,最后将其安装到您的项目中。
使用 MOQ 进行对象模拟
在上面的例子中,看第 35 和 41 行;这里首先,我们使用 Moq
创建对象的实例。
Mock<IBankAccount> mockCheckingAccount = new Mock<IBankAccount>();
Mock<IBankAccount> mockSavingAccount = new Mock<IBankAccount>();
在第 38 行和 44 行,我们正在实现一个虚拟实现,如果调用“GetMoneyByUserAccountNo
”方法,它将返回 5 和 6。因此,我们不需要知道它的任何内部逻辑。
mockCheckingAccount.Setup(a => a.GetMoneyByUserAccountNo(userAccountNumber)).Returns(5);
mockSavingAccount.Setup(a => a.GetMoneyByUserAccountNo(userAccountNumber)).Returns(6);
现在在第 48 行显示我们通过构造函数将模拟对象注入到类中。
SimpleMath simpleMath = new SimpleMath
(mockCheckingAccount.Object, mockSavingAccount.Object);
其次,在“Act”部分的第 56 行,我们正在调用验证方法,输出结果进入 actualSumResult
变量,最后在第 59 行,它给我们提供了实际的输出结果。
actualSumResult = simpleMath.GetTotalMoneyByUserAccount(userAccountNumber);
我的主要目标是向您介绍对象模拟的世界。所以,这只是一个例子。您可以使用任何模拟框架。
如何使用 Moq 序列调用测试方法的顺序
从 Github 下载 Moq.Sequences.dll,然后将 Moq.Sequences.dll 作为引用添加到您的 .NET 项目中,并在您的测试类中添加 using Moq.Sequences
;。或者只需使用“管理 NuGet 包”管理器并键入 "Moq.Sequences
" 并搜索它。最后,将其安装到您的项目并将其用作引用。
Moq.Sequences
支持以下内容
- 检查方法调用、属性获取和属性设置的顺序
- 允许您指定在预期下一个调用之前调用发生的次数
- 允许混合序列化和非序列化预期
- 线程安全——每个线程可以有自己的序列
按顺序调用方法的示例
[TestMethod]
public void Given_Valid_Data_For_Object_Model_When_TestMethod_
Is_Called_Then_It_Should_Be_Done_Successfully()
{
......
......
//// Methods are called sequentially.
using (Sequence.Create())
{
//// First, it is called
Given_Valid_Template_Data_When_MethodSave_Is_Called_
Then_It_Should_Be_Saved_Successfully();
//// Second, it is called
Given_Valid_Template_Value_When_MethodUpdate_Is_Called_
Then_It_Should_Update_Template();
///// Third, it is called
Given_Valid_Template_ID_When_DeleteTemplate_Is_Called_
Then_It_Should_Delete_Template();
}
......
......
}
[TestMethod]
public void Given_Valid_Data_For_Model_When_TestMethod_
Is_Called_Then_It_Should_Be_Done_Successfully()
{
......
......
///
using (Sequence.Create())
{
//// Methods are called sequentially.
mockObject.Setup(x => x.MethodA()).InSequence();
mockObject.Setup(x=>x.MethodB()).InSequence(Times.AtMostOnce());
mockObject.Setup(x=> x.MethodC()).InSequence(Times.Once());
}
......
......
}
历史
- 2017 年 4 月 17 日:初始版本