单元测试用例设计

结构测试

结构测试是一种白盒测试技术,开发人员以白盒方法根据代码的内部结构设计测试用例。 该方法需要识别代码中所有可能的路径。测试人员选择测试用例输入、执行它们并确定适当的输出。 主要结构测试技术包括:

  • 语句、分支和路径测试: 程序中的每个语句、分支或路径至少由测试执行一次。语句测试是最细粒度的选项
  • 条件测试: 允许开发人员通过基于值比较执行代码来选择性地确定测试执行的路径
  • 表达式测试: 针对正则表达式的不同值测试应用程序

功能测试

功能单元测试是一种用于测试应用程序组件功能的”黑盒”测试技术。 主要功能技术包括:

  • 输入域测试: 测试输入对象的大小和类型,并将对象与等价类进行比较。
  • 边界值分析: 测试旨在检查软件是否正确响应超出边界值的输入。
  • 语法检查: 检查软件是否正确解释输入语法的测试。
  • 等效分区: 一种软件测试技术,它将软件单元的输入数据划分为数据分区,并对每个分区应用测试用例。

错误测试

基于错误的单元测试最好由最初设计代码的开发人员构建。技术包括:

  • 错误播种:将已知错误放入代码中并进行测试,直到找到它们。
  • 突变测试:更改源代码中的某些语句,看看测试代码是否可以检测到错误。突变测试的运行成本很高,尤其是在非常大的应用程序中。
  • 历史测试数据:使用以前测试用例执行的历史信息来计算每个测试用例的优先级。

代码可测试性

参考资料

https://www.testrail.com/blog/highly-testable-code/

低耦合

泛泛而谈的低耦合

想成为百万富翁吗?好吧,请按照以下两个简单步骤操作:

  1. 做一个人
  2. 获得一百万美元 我们知道这只是一个笑话,但这正是一些人谈论“低耦合”的方式。他们说:“想编写可维护的代码吗?好吧,保持低耦合。” 有趣的是,他们似乎没有意识到:
    • 初学者对“低耦合”的含义一无所知
    • 即使他们知道,这也不足以让他们编写实践中的低耦合代码

耦合与低耦合

首先,我们来定义耦合。在软件开发中,这意味着一个给定的软件工件(一个方法、一个类,甚至一个模块)对另一个软件工件的依赖程度。 考虑到这一点,“低耦合”意味着代码的每个部分都应该尽可能少地了解代码的其他部分。

高耦合的问题

当您的代码是高耦合的时,其维护就会变得昂贵。 例如:您编辑一些被广泛使用的方法签名,这会产生连锁反应。 您很快就会意识到,您几乎已经接触了整个应用程序的代码。 让我们看一个简单的例子。假设您正在编写一个包含 ProductService 类的应用程序:

public class ProductService
{
// fields, properties, etc

public void SaveProduct(ProductToAddDTO productDTO)
{
Product product = MapToEntity(productDTO);
productRepository.Add(product);
productRepository.SaveChanges();
}

// more methods
}

上面的代码只是一个玩具。请允许我在这里发挥你的想象力来填写缺少的相关代码。 是的,代码工作正常,但现在有人决定它需要日志记录。你说,这很公平,然后你将代码更改为如下所示:

productRepository.Add(product);
productRepository.SaveChanges();
var logger = new FileLogger(@"logsapp-demo.log");
logger.Info($"The product with Id {product.Id} was saved.");

上面的代码有错吗?嗯,它可能有效,但有问题。上面的代码与 FileLogger 类紧密耦合。它需要了解得太多了。首先,它知道FileLogger类确实存在。 未来需求完全有可能(甚至很可能)再次发生变化,就像刚刚发生变化一样。两个月后,有人可能会决定代码应该记录到数据库而不是文件。 或者更糟糕的是,除了记录到文件之外,还记录到数据库。上面的代码还知道 FileLogger 的构造函数需要路径。 如果将来它也开始需要一个布尔标志来指示如果文件不存在是否应该创建该文件怎么办? 文件路径。路径、数据库字符串连接、XML 配置文件等都属于我所说的基础设施级别;业务代码逻辑执行中不需要知道。 哦,当然,上面的代码只是一个简单的例子。在一个规模很大的应用程序中,您可能有数百行引用 FileLogger 的代码。 每次开发人员对其进行更改时,都可能需要花费数小时的开发工作。

如何低耦合

有一个已知的解决这个问题的方法,它被称为依赖注入(DI)。 DI 包括通过其构造函数以接口的形式传递类所需的依赖项。 让我们重写示例,使用 DI 创建低耦合代码。首先,我们需要一个ILogger接口:

public interface ILogger
{
void Debug(string entry);
void Trace(string entry);
void Info(string entry);
void Warning(string entry);
void Error(string entry);
}

接口声明就位后,我们现在准备根据需要编写接口的 n 个实现。这里就不具体描述代码了。 最后,让我们编辑 ProductService 类:

public class ProductService
{
// fields, properties, etc

public ProductService(IProductRepository repo, ILogger logger)
{
this.repo = repo ?? throw new ArgumentNullException(nameof(repo));
this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
}

public void SaveProduct(ProductToAddDTO productDTO)
{
Product product = MapToEntity(productDTO);
this.repo.Add(product);
this.repo.SaveChanges();
this.logger.Info($"The product with Id {product.Id} was saved.");
}

// more methods
}

您肯定已经注意到,我不仅通过 ProductService 的构造函数注入记录器,而且还注入存储库 IProductRepository。 同样的推理也适用:只要实体被持久化,您的业务规则就不应该关心它们被持久化到哪里。 这样,ProductService 类现在完全可以进行单元测试,无需接触数据库或文件系统。

纯代码和非纯代码之间的明确区分

纯代码

这个概念是函数式编程范式不可或缺的一部分。面向对象的程序员也应该理解并利用它。 当我们谈论纯代码时,我们实际上是在谈论函数的纯粹性。 简而言之,纯函数是一种既不消耗也不产生副作用的函数。通过一个例子就会变得更清楚。考虑下面的代码:

public int Sum(int a, int b)
{
return a + b;
}

将两个数字相加感觉像是我能想到的最懒的例子,但它是纯函数的完美例证。

  • 它可以访问的唯一数据是作为参数获取的值
  • 它不会引起任何外部变化。它不会导致数据库、文件系统或任何外部现实发生变化。屏幕上没有显示任何内容,也没有在纸上打印任何内容。它不会在任何地方改变任何变量

非纯代码

一个不纯函数的示例:

public string GetGreeting()
{
var now = DateTime.Now;
var hour = now.Hour;
var greeting = string.Empty;

if (hour >= 6 && hour < 12)
{
greeting = "Good morning!";
}
else if (hour >= 12 && hour < 18)
{
greeting = "Good afternoon!";
}
else
{
greeting = "Good evening!";
}

greeting += " Today is " + now.ToString("D");
}

GetGreeting 方法绝对是不纯粹的。为什么?它从参数以外的源访问数据(参数甚至不存在)。 换句话说,这些差异可能看起来不多,但这些差异的后果可能是巨大的。

纯代码和非纯代码的差异

纯函数是确定性的。对于给定的输入,它将始终返回相同的输出。这使它安全。你可以放心地调用它一百万次,任何地方的任何事情都不会因此而改变。 纯函数本质上是可测试的。 相反,考虑一下您将如何测试非纯代码的 GetGreeting 函数。 Assert.AreEquals(“这里怎么描述期望输出呢???”, obj.GetGreeting()); 这绝对是可能的,甚至有多种方法可用,但这需要一些思考。

逻辑与表示的分离

对于任何一个已经从事该行业多年的值得信赖的软件开发人员来说,这一点确实不应该感到惊讶。 业务逻辑和表示之间的明确分离是一个值得追求的目标,即使您没有考虑可测试性。 保持这两个问题之间的严格分离允许您将用户界面层视为插件,根据需要将一种类型的界面替换为另一种类型的界面。 或者您可以拥有多个 UI,所有 UI 都使用相同的底层 API。 但分离逻辑和表示的真正好处在于测试。当您将所有业务逻辑包含在不关心 UI 等脆弱问题的库中时,您将拥有尽可能快速、健壮和可靠的单元测试。

保持简单

最后,我们来谈谈简单性。我所说的简单并不是一种模糊的、虚假的深刻的方式,这听起来好像在你阅读时有意义,但随后会让你摸不着头脑,想知道到底如何才能以实际的方式应用它。 不,我的意思是非常实用且可衡量的。简单代码是具有低圈复杂度的代码。 简而言之,圈复杂度是给定函数或方法中所有可能的执行分支的数量。具有高圈复杂度的方法将需要大量的测试用例才能正确测试。 如果降低代码的复杂性,代码不仅会变得更易于测试,而且会变得更干净、更可读、更容易维护。


测试类型

根据测试软件规格属性分类

功能测试

功能测试是针对软件应用的功能进行的测试,以确保它们按照需求规格说明书的要求执行。 功能测试是测试的核心,也是通常意义下所指的测试:

  • 运行软件,检查其行为(输出)是否符合预期
  • 给系统不同的输入,将其与规格说明书对比
  • 对软件进行探索式测试,检查是否违反了隐性需求

非功能测试

非功能测试关注软件的非功能方面,如性能、可用性、可靠性、响应时间和安全性。 这类测试评估软件系统在非功能需求上的表现。

根据测试是否关注软件内部分类

白盒测试

白盒测试是一种测试软件的方法,用于测试应用程序的内部结构或工作情况。白盒测试又称透明箱、玻璃箱、透明箱测试、结构测试。

  • 此类测试主要由开发人员完成
  • 测试内部结构
  • 所有独立路径至少遍历一次
  • 评估所有逻辑决策并确定它们是对还是错
  • 评估所有循环以检查其边界
  • 评估所有内部数据结构以确认其有效性

黑盒测试

黑盒测试是一种软件测试方法,它检查应用程序的功能(例如软件的功能),而无需深入了解其内部结构或工作原理。 黑盒测试完全是基于软件需求和规格说明书来进行的。

根据测试级别分类

单元测试

单元测试指对系统的一小部分而进行的测试。由于单元测试与代码存在固有耦合性,所以他们是开发人员编写,由单元测试框架执行。 单元测试大小是模糊的,可以是函数、方法、类,甚至是实现某些特定功能的一组相互协助的类。

集成测试

集成测试这一概念不仅模糊,而且有多重含义。 集成指的是集成多个系统还是多个模块?是系统测试还是单元测试? 这两种含义相差比较大,干系人可能并不相同,所以在使用集成测试这个名称时,需要确保大家理解一致。

验收测试

验收测试开始是指由最终用户实施的,证明软件符合预期,并为上线做准备的测试活动。 现在这个活动被称为用户验收测试。 验收测试现在通常指由框架执行的自动化黑盒测试,确保所有需求的用户故事都被正确实现。

根据开发模式区分

根据瀑布开发模式和敏捷开发模式,匹配的测试可以分为传统测试和敏捷测试

传统测试

传统上测试是一个验证的阶段,发生在软件完成构建之后。 broadcasting 测试者更容易跟开发人员形成分裂和对立。

敏捷测试

敏捷测试是一种支持敏捷开发的测试方式。在敏捷测试中,测试者参加团队的质量核心小组,并且以任何方式为成功发布做出贡献,而不是仅仅编写测试用例。 敏捷团队的每一个人都承担把客户需要的功能转变到软件里去的责任,而测试者花更多的时间在客户身上,他们的角色就是帮助澄清需求和设计测试用例。 这两个工作都需要非常熟悉业务规则。

敏捷测试四象限

其他一些测试类型

  • 冒烟测试:
    1. 将烟吹进管道,测试管道是否有裂缝
    2. 电路板通电测试,如果接上电源就冒烟,其他的测试就不用做了
    3. 软件行业是指系统部署后,立即执行系统最基本功能的快速测试
  • 端到端测试
    1. 系统测试,特指通过系统将整改执行路径或过程都包含进来
    2. 在不同软件产品或不同团队中,对于系统和系统的边界定义并不相同
  • 特性测试:测试软件的变更是基于什么需求开发的
  • 正面、负面测试
    1. 正面测试:验证按照约定输入被测对象会像预期的那样工作和表现。又称为主逻辑路径(happy path)
    2. 负面测试:验证在输入无效值的情况下,系统运行符合预期,不会产生意想不到的行为
  • 小型、中型、大型测试:谷歌对测试的划分
    1. 小型测试:聚焦于单一函数或模块,通常是单元测试;速度快,60s内完成,执行频繁
    2. 中型测试:检查不同层次之间的交互,可以使用数据库,访问文件系统和测试多线程代码。不能访问外部系统和远程主机;执行时间不超过300s
    3. 大型测试:不受任何限制

开发者测试目标

测试工具

工具是辅助测试过程必备的,包括自动化测试框架、环境这些工具,都极大的降低手工测试的难度和繁琐程度。 并且,测试工具可以帮助开发人员检查对于一段自己所写代码的一些假设。 我们通过工具进行测试,会受到工具功能和程序的限制;一般基于工具的测试可能会在第一次发现,后续重复执行时很少会发现新的缺陷以及产生新的洞察了。 通过工具进行测试适合发现回归缺陷以及验证已经存在的假设(设计的用例)。

测试目标

  • 批判测试:开发已经完成,需要对软件进行评估。 主要是:”软件是否满足规格”、”软件有什么缺陷”; 还有:软件功能是否合理,是否遗漏任何功能,软件运行是否足够快,软件是否符合法律规范
  • 支持测试:提供质量反馈,帮助团队对开发软件获得短期和长期的质量信心。 重点在于,软件实现过程中,尽可能快的获得质量信息,用于支持团队的开发活动。

开发者测试的目标

开发者测试更多的是依赖工具,甚至开发工具以完成测试内容和测试自动化,用来确保预期假设的回归和验证。 开发者测试主要集中在支持测试:通过实现自动化测试,测试驱动开发,稳定开发过程和错误预防活动。


—  原创作品许可 — 署名-非商业性使用-禁止演绎 3.0 未本地化版本 — CC BY-NC-ND 3.0   —