软件测试——编写单元测试的最佳实践和原则






4.79/5 (22投票s)
软件测试概念、最佳实践和原则基础
软件测试
此主题的读者 - 开发人员,应用架构师。
您将从本主题中学到什么
我们知道单元测试和集成测试的区别。但有时,我们会将单元测试与集成测试混淆。但这并不符合预期。我将解释编写单元测试的原则和最佳实践,以及如何在白盒测试期间使用行为驱动开发(BDD)编写测试用例。以下概念将涵盖
- 白盒和黑盒
- 单元测试的优点和缺点
- 编写单元测试的原则
- 编写单元测试的最佳实践
- 编写测试场景
- 编写测试用例
- 编写测试数据
- 传统单元测试原则
- 测试方法命名约定
- 为什么要进行行为驱动开发(BDD)
- 单元测试无紧耦合
软件测试基本概述
软件测试的好处
- 降低系统迁移到生产环境或上线运行时的故障风险
- 业务需求已满足的文档证明
- 确保用户能够富有成效地操作设计的解决方案
- 确保系统与现有遗留系统正常协同工作
软件测试员 - 开发人员 VS. QA
开发人员的观点
开发人员可以根据业务需求验证他们实现的逻辑或代码。他们确保所有逻辑都能顺利运行。另一方面,他们会确认如果最终用户传递了任何非预期数据,逻辑仍然可以处理异常,并显示适当的消息。这就是所谓的白盒测试,因为他们可以看到内部逻辑或代码。例如,自动化单元测试或自动化集成测试。
QA 的观点
质量保证(QA)团队可以根据业务需求验证软件。但他们不必担心逻辑或代码。他们以最终用户的身份测试功能。他们将输入正向数据以验证正向功能。另一方面,他们将输入错误的数据以确保他们获得错误输入的正确消息。这就是所谓的黑盒测试,因为他们看不到内部逻辑或代码。他们验证外部行为。例如,验收测试。
让我们深入了解基本的测试概念
测试文档基础
- 测试用例 – 一组条件和步骤,用于确定系统是否正确满足需求。这由测试数据、环境和预期结果描述。
- 测试套件 – 一组测试用例。
- 测试计划 – 描述测试方法、测试套件和测试用例的文档。
- 测试策略 – 一种从规范中识别测试用例的方法。
- 测试有效性 – 测试策略发现错误的相对能力。
- 测试覆盖率 – 已测试的可测试元素的百分比。
测试场景与测试用例
如果你必须从 ATM 机上取钱,那么这是一个场景。但是要取钱,你需要执行许多测试用例。
测试场景 - 一个测试场景可以包含多个测试用例。因此,在开始测试时,首先准备测试场景,然后为每个场景创建测试用例。
测试场景示例:检查登录按钮的功能。
测试用例 - 测试用例是在预定义步骤集和已知输入下,为预期输出而执行的条件。
测试用例示例
测试用例 1
:不输入用户名和密码,点击按钮。测试用例 2
:只输入用户名,点击按钮。测试用例 3
:输入错误的用户名和错误的密码,点击按钮,依此类推。
所有这些测试用例都将具有一些预期、非预期和实际结果。
错误术语
- 错误 - 开发人员在实现软件系统期间犯的错误
- 故障 - 程序的错误行为
- 缺陷 - 导致故障的错误代码
- 事故 - 与故障相关的症状
- Bug - 错误或缺陷
- 缺陷导向测试 – 通过故障发现 Bug
正面测试和负面测试
正面测试:当测试人员从正面视角使用有效输入/数据测试应用程序时,这被称为正面测试。
负面测试:当测试人员从负面视角使用无效输入/数据测试应用程序时,这被称为负面测试。
白盒与黑盒测试
根据开发人员和 QA 团队,我们可以说有两种类型的测试。开发人员可以看到已实现应用程序系统的内部逻辑或开发框。这个框对开发人员可见,称之为白盒。另一方面,QA 团队看不到逻辑实现,他们可以看到系统作为最终用户视角的功能。这就是为什么盒子是黑色的,他们测试外部行为。这称为黑盒测试。
白盒测试的优缺点
优点
- 揭示隐藏代码中的错误
- 强制为正面、负面和异常情况的实现进行推理
- 获得信心和文档证明
- 改进设计和代码质量
缺点
- 与时间和设计相比昂贵
- 需要对测试方法论有充分的了解
- 遗漏的案例可能会遗漏代码
黑盒测试的优缺点
优点
- 测试人员和程序员是独立的
- 从用户角度进行测试
- 规范确定后即可准备测试用例
缺点
- 测试人员和程序员的测试用例重复
- 只能进行简单测试,因为所有情况都无法构建和测试
黑盒 vs. 白盒测试方法
黑盒方法
- 字段级别检查
- 字段级别验证
- 用户界面检查
- 功能级别检查
白盒方法
- 语句覆盖
- 决策覆盖
- 条件覆盖
- 路径覆盖
测试类型
- 单元测试
- 集成测试
- 功能测试
- 系统测试
- 压力测试
- 性能测试
- 可用性测试
- 验收测试
- 回归测试
- Beta 测试
许多组织中最常见的测试
粒度级别
- 单元测试 - 单个类的验证
- 模块测试 - 测试类组(包)的交互
- 集成测试 - 验证模块/组件之间的交互
- 功能测试 - 验证模块、组件和系统的外部行为
- 系统测试 - 根据目标测试系统
- 验收测试 - 根据用户需求验证应用程序
- 回归测试 - 系统更改时重新运行所有测试
停止单元测试
不进行单元测试的前 5 个借口
- 我没时间做单元测试。
- 办公室付我薪水是为了写代码,而不是写单元测试。
- 我正在支持一个没有单元测试的遗留应用程序,并且现有设计不适合单元测试。
- QA 和用户验收测试在发现 Bug 方面更有效。
- 我不知道如何编写单元测试。
单元测试的优点
- 减少生产代码中的 Bug 数量
- 节省您的开发时间
- 自动化测试可以根据需要频繁运行
- 使代码的更改和重构更容易
- 改善代码设计,尤其是在测试驱动开发(Test-Driven-Development)下
- 一种文档形式
- 激发信心!
- 完成度衡量
单元测试的缺点
- 无法实现逻辑运算符
- 对循环运算符(迭代次数)不敏感
编写单元测试的最佳实践和原则
编写单元测试的原则
原则 1。“只测试类的逻辑,仅此而已”
请注意,这是单元测试中最重要的原则之一。当您要测试一个类时,您不应该依赖数据库、文件、注册表、Web 服务等。您应该能够完全“隔离”地测试任何类,并且它应该设计为支持完全“隔离”。
根据此原则,模拟所有外部服务和状态,并记住单元测试从不使用-
- 配置文件
- 数据库
- 其他应用程序/服务/文件/网络 I/O
- 日志记录
原则 2。“先失败,守门”
在编写代码片段之前,先编写失败的测试。之后,添加或重构您的守门逻辑,并为负面或异常数据抛出适当的异常或消息。最后,运行它并通过测试。
单元测试原则解释
原则 1:假设您有一个名为 'IsValidUser
' 的方法来验证登录的用户名和密码。
现在,如果您查看第 15 行和第 17 行,您会看到类'UserLogIn
'依赖于类'UserDataAccess
',并且它是紧密耦合的。现在的问题是,您需要测试类'UserLogIn
'中的方法'IsValidUser
',并且您必须避免依赖。因为如果您调用'IsValidUser
',它将调用方法'GetUserInfoByUseName
'。根据单元测试原则,如果我们从一个方法到另一个外部类的依赖关系,那么我们必须为该外部类模拟对象。这意味着我们必须向外部类对象注入一些虚拟数据。所以,在这个例子中,我们需要为'GetUserInfoByUseeName(userName)
'注入虚拟数据。如果我们这样做,那么我们将能够验证'UserLogIn
'类中'IsValidUser
'方法的逻辑。
因此,避免紧耦合代码并重新重构代码。查找我的另一篇文章,了解依赖关系、紧耦合和松耦合。现在我的主要目标是只让您了解单元测试原则。无论如何,让我们重构这个类。
现在类'RefactoringUserLogIn
'是松耦合的,您可以注入'IUserDataAccess
'接口的实现类,这将使您的单元测试生活变得轻松。请注意,注入实现类的方法有很多种。查找我的另一篇文章,了解如何模拟类的对象。在单元测试期间,如果您模拟'IUserDataAccess
',这意味着您正在实现'IUserDataAccess
'接口的虚拟对象,并且该方法'IsValidUser
'将无法直接从外部(如数据库或 ORM)通过'UserDataAccess
'类获取任何数据。
原则 2
第一次,超过 50% 的人会挂掉他们的驾照考试,然后在高速公路上练习,寻求他人的建议,他们确保了公路考试的成功。无论如何,如果您已经熟悉测试驱动开发(TDD),那么您已经熟悉了失败的测试。我将在另一篇文章中介绍 TDD。
让我们解释守门逻辑以及它将如何帮助您通过单元测试。假设我们有一个名为‘GetSumByPositiveNumber
’的方法,它有两个参数。业务需求始终输入正数并返回总和。切勿输入负数或零。
现在根据您的业务需求,如果您传递参数值(例如 1 和 2),那么该方法将返回 3,这是正确的。但是,如果您传递负值作为参数,或者像(-5, -1)、(-5, 1)、(5, 0)、(0, 0)等的组合呢?
但是,如果我们一开始就在方法中加入一些守门逻辑,那么测试应该会通过。因为我们正在为负面数据抛出异常。
编写单元测试的最佳实践
- 如果旧测试因引入的方法扩展而失败,则删除或增强旧测试并避免需求冲突。
- 如果您删除了被测试类中的一行代码,而所有测试仍然通过,那么您就没有足够的单元测试。
- 开发人员通常会运行测试,所以让测试易于运行。
- 仅测试组件的
public
方法和public
接口 - 不要在单元测试中直接创建类的实例 - 使用工厂方法。
- 重构单元测试并保持简单。记住测试必须由任何代码维护 - 因此最好保持简单。
- 任何测试都应按任意顺序运行;因此,避免测试之间的依赖关系。
- 测试应该像书一样易于阅读;因此,在断言中添加注释。编写描述性的方法名称。使用行为驱动开发(BBD)技术。
- 测试所有可能出错的地方。
- 测试一切,但不要测试
Private
方法
编写测试场景、测试用例和测试数据
单元测试仅需 3 个测试用例,仅此而已
- 正面测试用例:正确的数据以检查正确的输出
- 负面测试用例:损坏或缺失的数据以检查正确处理
- 异常测试用例:提供意外数据或行为,并检查异常是否被正确捕获
根据测试用例的测试数据
让我们设置一些测试数据,并以“GetSum
”为例。这里,我的主要目标是向您展示如何为测试用例设置一些测试数据。
测试数据类型
- 正面数据
- 负面数据
- 异常数据
正面测试用例的正面数据
正面测试用例的主要目标是验证逻辑的功能。
测试用例-1
:给定正值,应返回预期结果
测试数据-1
:设置输入参数为firstNumber =1
,secondNumber=1
负面测试用例的负面数据
负面测试用例的主要目标是根据您的业务需求,为不良输入获得正确的消息。
测试用例-2
:给定无效值,应生成无效参数消息
测试数据-2
:设置输入参数为firstNumber =-1
,secondNumber =-1
测试数据-3
:设置输入参数为firstNumber =-1
,secondNumber = 1
测试数据-4
:设置输入参数为firstNumber = 0
,secondNumber = 1
测试数据-5
:设置输入参数为firstNumber = 0
,secondNumber =-1
测试数据-6
:设置输入参数为firstNumber = 0
,secondNumber = 0
, 等等。
异常测试用例的异常数据
异常测试用例的主要目标是找出具有正确消息的正确异常处理。这样您的代码就不会因为阈值而崩溃。
在这里,您可以设置测试数据的阈值。在 DOT NET 中,int
类型变量的最小值是-2147483648
,int
类型变量的最大值是2147483647
。
测试用例 3
:给定阈值限制值,应抛出异常消息
测试数据-7
:设置输入参数为firstNumber =1
,secondNumber =2147483649
测试数据-8
:设置输入参数为firstNumber =1
,secondNumber = -2147483649
测试数据-9
:设置输入参数为firstNumber =2147483649
,secondNumber = -2147483649
测试数据-10
:设置输入参数为firstNumber =2147483647
,secondNumber=2147483647
, 等等。
单元测试的测试方法命名约定
单元测试的传统原则
一个测试方法仅用于测试一个方法,一个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
”编写测试方法
现在根据传统原则,我们已经用“测试数据-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
更易读 -
鉴于_Zero_Values_As_Params_当_GetSum_Is_Called_那么_It_Should_Throw_Invalid_Argument_Exception
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
更易读 -
鉴于_Max_Limit_Values_Of_Int_As_Params_当_GetSum_Is_Called_那么_It_Should_Throw_Sum_Exception
历史
- 2017 年 4 月 12 日:初始版本