65.9K
CodeProject 正在变化。 阅读更多。
Home

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

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.79/5 (22投票s)

2017年4月12日

CPOL

14分钟阅读

viewsIcon

28910

软件测试概念、最佳实践和原则基础

软件测试

此主题的读者 - 开发人员,应用架构师。

您将从本主题中学到什么

我们知道单元测试和集成测试的区别。但有时,我们会将单元测试与集成测试混淆。但这并不符合预期。我将解释编写单元测试的原则和最佳实践,以及如何在白盒测试期间使用行为驱动开发(BDD)编写测试用例。以下概念将涵盖

  • 白盒和黑盒
  • 单元测试的优点和缺点
  • 编写单元测试的原则
  • 编写单元测试的最佳实践
  • 编写测试场景
  • 编写测试用例
  • 编写测试数据
  • 传统单元测试原则
  • 测试方法命名约定
  • 为什么要进行行为驱动开发(BDD)
  • 单元测试无紧耦合

软件测试基本概述

软件测试的好处

  • 降低系统迁移到生产环境或上线运行时的故障风险
  • 业务需求已满足的文档证明
  • 确保用户能够富有成效地操作设计的解决方案
  • 确保系统与现有遗留系统正常协同工作

软件测试员 - 开发人员 VS. QA

开发人员的观点

开发人员可以根据业务需求验证他们实现的逻辑或代码。他们确保所有逻辑都能顺利运行。另一方面,他们会确认如果最终用户传递了任何非预期数据,逻辑仍然可以处理异常,并显示适当的消息。这就是所谓的白盒测试,因为他们可以看到内部逻辑或代码。例如,自动化单元测试或自动化集成测试。

QA 的观点

质量保证(QA)团队可以根据业务需求验证软件。但他们不必担心逻辑或代码。他们以最终用户的身份测试功能。他们将输入正向数据以验证正向功能。另一方面,他们将输入错误的数据以确保他们获得错误输入的正确消息。这就是所谓的黑盒测试,因为他们看不到内部逻辑或代码。他们验证外部行为。例如,验收测试。

让我们深入了解基本的测试概念

测试文档基础

  • 测试用例 – 一组条件和步骤,用于确定系统是否正确满足需求。这由测试数据、环境和预期结果描述。
  • 测试套件 – 一组测试用例。
  • 测试计划 – 描述测试方法、测试套件和测试用例的文档。
  • 测试策略 – 一种从规范中识别测试用例的方法。
  • 测试有效性 – 测试策略发现错误的相对能力。
  • 测试覆盖率 – 已测试的可测试元素的百分比。

测试场景与测试用例

如果你必须从 ATM 机上取钱,那么这是一个场景。但是要取钱,你需要执行许多测试用例。

测试场景 - 一个测试场景可以包含多个测试用例。因此,在开始测试时,首先准备测试场景,然后为每个场景创建测试用例。

测试场景示例:检查登录按钮的功能。

测试用例 - 测试用例是在预定义步骤集和已知输入下,为预期输出而执行的条件。

测试用例示例

  • 测试用例 1:不输入用户名和密码,点击按钮。
  • 测试用例 2:只输入用户名,点击按钮。
  • 测试用例 3:输入错误的用户名和错误的密码,点击按钮,依此类推。

所有这些测试用例都将具有一些预期、非预期和实际结果。

错误术语

  • 错误 - 开发人员在实现软件系统期间犯的错误
  • 故障 - 程序的错误行为
  • 缺陷 - 导致故障的错误代码
  • 事故 - 与故障相关的症状
  • Bug - 错误或缺陷
  • 缺陷导向测试 – 通过故障发现 Bug

正面测试和负面测试

正面测试:当测试人员从正面视角使用有效输入/数据测试应用程序时,这被称为正面测试。

负面测试:当测试人员从负面视角使用无效输入/数据测试应用程序时,这被称为负面测试。

白盒与黑盒测试

根据开发人员和 QA 团队,我们可以说有两种类型的测试。开发人员可以看到已实现应用程序系统的内部逻辑或开发框。这个框对开发人员可见,称之为白盒。另一方面,QA 团队看不到逻辑实现,他们可以看到系统作为最终用户视角的功能。这就是为什么盒子是黑色的,他们测试外部行为。这称为黑盒测试。

白盒测试的优缺点

优点
  • 揭示隐藏代码中的错误
  • 强制为正面、负面和异常情况的实现进行推理
  • 获得信心和文档证明
  • 改进设计和代码质量
缺点
  • 与时间和设计相比昂贵
  • 需要对测试方法论有充分的了解
  • 遗漏的案例可能会遗漏代码

黑盒测试的优缺点

优点
  • 测试人员和程序员是独立的
  • 从用户角度进行测试
  • 规范确定后即可准备测试用例
缺点
  • 测试人员和程序员的测试用例重复
  • 只能进行简单测试,因为所有情况都无法构建和测试

黑盒 vs. 白盒测试方法

黑盒方法
  • 字段级别检查
  • 字段级别验证
  • 用户界面检查
  • 功能级别检查
白盒方法
  • 语句覆盖
  • 决策覆盖
  • 条件覆盖
  • 路径覆盖

测试类型

  • 单元测试
  • 集成测试
  • 功能测试
  • 系统测试
  • 压力测试
  • 性能测试
  • 可用性测试
  • 验收测试
  • 回归测试
  • Beta 测试

许多组织中最常见的测试

粒度级别

  • 单元测试 - 单个类的验证
  • 模块测试 - 测试类组(包)的交互
  • 集成测试 - 验证模块/组件之间的交互
  • 功能测试 - 验证模块、组件和系统的外部行为
  • 系统测试 - 根据目标测试系统
  • 验收测试 - 根据用户需求验证应用程序
  • 回归测试 - 系统更改时重新运行所有测试

停止单元测试

不进行单元测试的前 5 个借口

  1. 我没时间做单元测试。
  2. 办公室付我薪水是为了写代码,而不是写单元测试。
  3. 我正在支持一个没有单元测试的遗留应用程序,并且现有设计不适合单元测试。
  4. QA 和用户验收测试在发现 Bug 方面更有效。
  5. 我不知道如何编写单元测试。
单元测试的优点
  • 减少生产代码中的 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)等的组合呢?

但是,如果我们一开始就在方法中加入一些守门逻辑,那么测试应该会通过。因为我们正在为负面数据抛出异常。

编写单元测试的最佳实践

  1. 如果旧测试因引入的方法扩展而失败,则删除或增强旧测试并避免需求冲突。
  2. 如果您删除了被测试类中的一行代码,而所有测试仍然通过,那么您就没有足够的单元测试。
  3. 开发人员通常会运行测试,所以让测试易于运行。
  4. 仅测试组件的public方法和public接口
  5. 不要在单元测试中直接创建类的实例 - 使用工厂方法。
  6. 重构单元测试并保持简单。记住测试必须由任何代码维护 - 因此最好保持简单。
  7. 任何测试都应按任意顺序运行;因此,避免测试之间的依赖关系。
  8. 测试应该像书一样易于阅读;因此,在断言中添加注释。编写描述性的方法名称。使用行为驱动开发(BBD)技术。
  9. 测试所有可能出错的地方。
  10. 测试一切,但不要测试Private方法

编写测试场景、测试用例和测试数据

单元测试仅需 3 个测试用例,仅此而已

  1. 正面测试用例:正确的数据以检查正确的输出
  2. 负面测试用例:损坏或缺失的数据以检查正确处理
  3. 异常测试用例:提供意外数据或行为,并检查异常是否被正确捕获

根据测试用例的测试数据

让我们设置一些测试数据,并以“GetSum”为例。这里,我的主要目标是向您展示如何为测试用例设置一些测试数据。

测试数据类型

  1. 正面数据
  2. 负面数据
  3. 异常数据
正面测试用例的正面数据

正面测试用例的主要目标是验证逻辑的功能。

测试用例-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 类型变量的最小值是-2147483648int 类型变量的最大值是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:给定正值,应返回预期结果
  • 测试数据-1firstValue =5, secondValue =6
负面测试用例
  • TC2:给定零值,应生成无效参数消息
  • 测试数据-2firstValue =0, secondValue =0
  • TC3:给定负值,应生成无效参数消息
  • 测试数据-3firstValue =-5, secondValue =-6
异常测试用例
  • TC4:给定阈值限制值,应抛出异常消息
  • 测试数据-4firstValue =2147483647, secondValue =2147483647
测试方法示例

现在根据传统原则,让我们为“GetSum”编写测试方法

现在根据传统原则,我们已经用“测试数据-1”覆盖了正面测试用例。但是负面和异常测试用例呢??

我们如何用传统原则来覆盖负面和异常测试用例??

行为驱动开发(BDD)

为什么要进行 BDD

如果我们想根据我们先前的例子覆盖我们测试用例的所有行为,那么我们需要遵循一些技术,以便我们可以写下方法的所有行为。因此,BDD 是一种技术,它为我们提供了机会,通过标准且易于阅读的命名约定来满足所有测试用例。人多,想法多。有很多技术可以编写测试方法的命名约定。但它真的取决于您和您的偏好。如果您遵循其他技术,并没有对错之分。总之,我们可以说 BDD 中的组件测试其预期行为。

BDD 概念

  1. 鉴于我是一名 BDD 技术初学者,并且以前从未用过这项技术
  2. 我阅读这篇关于 BDD 的教程时
  3. 那么我开始喜欢它,并最终学会了它。

BDD 命名约定

测试场景

验证“GetSum”方法。

测试用例
正面测试用例
  • TC1:给定正值,应返回预期结果
  • 测试数据-1firstValue =5, secondValue =6

测试方法 - 命名约定

  • 鉴于PositiveVaidValuesAsParams_GetSumIsCalled_那么ItShouldReturnSumValue

更易读

  • 鉴于_Positive_Vaid_Values_As_Params__GetSum_Is_Called_那么_It_Should_Return_Sum_Value

负面测试用例
  • TC2:给定零值,应生成无效参数消息
  • 测试数据-2firstValue =0, secondValue =0

测试方法 - 命名约定

鉴于ZeroValuesAsParams_GetSumIsCalled_那么ItShouldThrowInvalidArgumentException

更易读 -

鉴于_Zero_Values_As_Params__GetSum_Is_Called_那么_It_Should_Throw_Invalid_Argument_Exception

  • TC3:给定负值,应生成无效参数消息
  • 测试数据-3firstValue =-5, secondValue =-6

测试方法 - 命名约定

鉴于NegativeValues_GetSumIsCalled_那么ItShouldThrowInvalidArgumentException

更易读 -

鉴于_Negative_Values__GetSum_Is_Called_那么_It_Should_Throw_Invalid_Argument_Exception

异常测试用例
  • TC4:给定阈值限制值,应抛出异常消息
  • 测试数据-4firstValue =2147483647, secondValue =2147483647

鉴于MaxLimitValuesOfIntAsParams_GetSumIsCalled_那么ItShouldThrowSumException

更易读 -

鉴于_Max_Limit_Values_Of_Int_As_Params__GetSum_Is_Called_那么_It_Should_Throw_Sum_Exception

历史

  • 2017 年 4 月 12 日:初始版本
© . All rights reserved.