几年前我写过这个,但不太详细。这是同一想法的更精致的版本。
简介
单元测试对开发人员来说既是福也是祸。它们允许快速测试功能、可读的使用示例、快速实验所涉及组件的场景。但它们也可能变得混乱,需要在每次代码更改时进行维护和更新,并且如果懒惰地完成,则无法隐藏错误而不是揭示错误。
我认为单元测试如此困难的原因是它与测试相关,而不是代码编写,而且单元测试的编写方式与我们编写的大多数其他代码相反。
在这篇文章中,我将为您提供一种编写单元测试的简单模式,该模式将增强所有好处,同时消除与正常代码的大部分认知失调。单元测试将保持可读性和灵活性,同时减少重复代码并且不添加额外的依赖项。
如何进行单元测试
但首先,让我们定义一个好的单元测试套件。
要正确测试一个类,必须以某种方式编写它。在这篇文章中,我们将介绍使用构造函数注入进行依赖项的类,这是我推荐的进行依赖项注入的方法。
然后,为了测试它,我们需要:
- 涵盖积极的场景 - 当类执行其应该执行的操作时,使用设置和输入参数的各种组合来覆盖整个功能
- 涵盖负面场景 - 当设置或输入参数错误时,类以正确的方式失败
- 模拟所有外部依赖
- 将所有测试设置、操作和断言保留在同一个测试中(通常称为 Arrange-Act-Assert 结构)
但这说起来容易做起来难,因为它还意味着:
- 为每个测试设置相同的依赖项,从而复制和粘贴大量代码
- 设置非常相似的场景,两次测试之间仅进行一次更改,再次重复大量代码
- 什么都不概括和封装,这是开发人员通常在所有代码中所做的事情
- 为很少的正例写了很多负例,感觉就像测试代码比功能代码多
- 必须为测试类的每次更改更新所有这些测试
谁喜欢这个?
解决方案
解决方案是使用构建器软件模式在 Arrange-Act-Assert 结构中创建流畅、灵活且可读的测试,同时将设置代码封装在一个类中,以补充特定服务的单元测试套件。我称之为 MockManager 模式。
让我们从一个简单的例子开始:
// the tested class public class Calculator { private readonly ITokenParser tokenParser; private readonly IMathOperationFactory operationFactory; private readonly ICache cache; private readonly ILogger logger; public Calculator( ITokenParser tokenParser, IMathOperationFactory operationFactory, ICache cache, ILogger logger) { this.tokenParser = tokenParser; this.operationFactory = operationFactory; this.cache = cache; this.logger = logger; } public int Calculate(string input) { var result = cache.Get(input); if (result.HasValue) { logger.LogInformation("from cache"); return result.Value; } var tokens = tokenParser.Parse(input); IOperation operation = null; foreach(var token in tokens) { if (operation is null) { operation = operationFactory.GetOperation(token.OperationType); continue; } if (result is null) { result = token.Value; continue; } else { if (result is null) { throw new InvalidOperationException("Could not calculate result"); } result = operation.Execute(result.Value, token.Value); operation = null; } } cache.Set(input, result.Value); logger.LogInformation("from operation"); return result.Value; } }
这是一个计算器,按照传统。它接收一个字符串并返回一个整数值。它还缓存特定输入的结果,并记录一些内容。实际操作由 IMathOperationFactory 抽象,输入字符串由 ITokenParser 转换为标记。别担心,这不是一个真正的课程,只是一个例子。让我们看一个“传统”测试:
[TestMethod] public void Calculate_AdditionWorks() { // Arrange var tokenParserMock = new Mock<itokenparser>(); tokenParserMock .Setup(m => m.Parse(It.IsAny<string>())) .Returns( new List<calculatortoken> { CalculatorToken.Addition, CalculatorToken.From(1), CalculatorToken.From(1) } ); var mathOperationFactoryMock = new Mock<imathoperationfactory>(); var operationMock = new Mock<ioperation>(); operationMock .Setup(m => m.Execute(1, 1)) .Returns(2); mathOperationFactoryMock .Setup(m => m.GetOperation(OperationType.Add)) .Returns(operationMock.Object); var cacheMock = new Mock<icache>(); var loggerMock = new Mock<ilogger>(); var service = new Calculator( tokenParserMock.Object, mathOperationFactoryMock.Object, cacheMock.Object, loggerMock.Object); // Act service.Calculate(""); //Assert mathOperationFactoryMock .Verify(m => m.GetOperation(OperationType.Add), Times.Once); operationMock .Verify(m => m.Execute(1, 1), Times.Once); } </ilogger></icache></ioperation></imathoperationfactory></calculatortoken></string></itokenparser>
让我们稍微打开一下它。例如,即使我们实际上并不关心记录器或缓存,我们也必须为每个构造函数依赖项声明一个模拟。在操作工厂的情况下,我们还必须设置一个返回另一个模拟的模拟方法。
在这个特定的测试中,我们主要编写了设置、一行 Act 和两行 Assert。此外,如果我们想测试缓存在类中的工作原理,我们必须复制粘贴整个内容,然后更改我们设置缓存模拟的方式。
还有一些负面测试需要考虑。我见过许多负面测试做了类似的事情:“设置应该失败的内容。测试它失败”,这引入了很多问题,主要是因为它可能会因完全不同的原因而失败,并且大多数时候这些测试遵循类的内部实现而不是其要求。正确的阴性测试实际上是完全阳性的测试,只有一个错误的条件。为了简单起见,这里的情况并非如此。
所以,言归正传,这里是相同的测试,但使用了 MockManager:
[TestMethod] public void Calculate_AdditionWorks_MockManager() { // Arrange var mockManager = new CalculatorMockManager() .WithParsedTokens(new List<calculatortoken> { CalculatorToken.Addition, CalculatorToken.From(1), CalculatorToken.From(1) }) .WithOperation(OperationType.Add, 1, 1, 2); var service = mockManager.GetService(); // Act service.Calculate(""); //Assert mockManager .VerifyOperationExecute(OperationType.Add, 1, 1, Times.Once); } </calculatortoken>
拆包,没有提到缓存或记录器,因为我们不需要在那里进行任何设置。一切都已打包且可读。复制粘贴此内容并更改一些参数或某些行不再难看。 Arrange 中执行了三种方法,一种在 Act 中执行,一种在 Assert 中执行。仅抽象了实质的模拟细节:这里没有提及 Moq 框架。事实上,无论决定使用哪种模拟框架,此测试看起来都是一样的。
让我们看一下 MockManager 类。现在这会显得很复杂,但请记住,我们只编写一次并多次使用它。该类的整体复杂性是为了使单元测试易于人类阅读,易于理解、更新和维护。
public class CalculatorMockManager { private readonly Dictionary<operationtype>> operationMocks = new(); public Mock<itokenparser> TokenParserMock { get; } = new(); public Mock<imathoperationfactory> MathOperationFactoryMock { get; } = new(); public Mock<icache> CacheMock { get; } = new(); public Mock<ilogger> LoggerMock { get; } = new(); public CalculatorMockManager WithParsedTokens(List<calculatortoken> tokens) { TokenParserMock .Setup(m => m.Parse(It.IsAny<string>())) .Returns( new List<calculatortoken> { CalculatorToken.Addition, CalculatorToken.From(1), CalculatorToken.From(1) } ); return this; } public CalculatorMockManager WithOperation(OperationType operationType, int v1, int v2, int result) { var operationMock = new Mock<ioperation>(); operationMock .Setup(m => m.Execute(v1, v2)) .Returns(result); MathOperationFactoryMock .Setup(m => m.GetOperation(operationType)) .Returns(operationMock.Object); operationMocks[operationType] = operationMock; return this; } public Calculator GetService() { return new Calculator( TokenParserMock.Object, MathOperationFactoryMock.Object, CacheMock.Object, LoggerMock.Object ); } public CalculatorMockManager VerifyOperationExecute(OperationType operationType, int v1, int v2, Func<times> times) { MathOperationFactoryMock .Verify(m => m.GetOperation(operationType), Times.AtLeastOnce); var operationMock = operationMocks[operationType]; operationMock .Verify(m => m.Execute(v1, v2), times); return this; } } </times></ioperation></calculatortoken></string></calculatortoken></ilogger></icache></imathoperationfactory></itokenparser></operationtype>
测试类所需的所有模拟都被声明为公共属性,允许对单元测试进行任何自定义。有一个 GetService 方法,它将始终返回被测试类的实例,并且所有依赖项都完全模拟。然后还有 With* 方法,它们自动设置各种场景并始终返回模拟管理器,以便可以链接它们。您还可以使用特定的断言方法,尽管在大多数情况下您会将一些输出与预期值进行比较,因此这些只是为了抽象出 Moq 框架的Verify 方法。
结论
此模式现在使测试编写与代码编写保持一致:
- 抽象出任何上下文中你不关心的事物
- 一次编写,多次使用
- 人类可读的自记录代码
- 低圈复杂度的小方法
- 直观的代码编写
现在编写单元测试既简单又一致:
- 实例化您要测试的类的模拟管理器(或根据上述步骤编写一个)
- 为测试编写特定场景(自动完成现有已涵盖的场景步骤)
- 使用测试参数执行你想要测试的方法
- 检查一切是否符合预期
抽象并不止于模拟框架。相同的模式可以应用于每种编程语言!对于 TypeScript 或 JavaScript 或其他东西来说,模拟管理器构造将非常不同,但单元测试看起来几乎是一样的。
希望这有帮助!
以上是单元测试中的 MockManager - 用于模拟的构建器模式的详细内容。更多信息请关注PHP中文网其他相关文章!

C#和C 在面向对象编程(OOP)中的实现方式和特性上有显着差异。 1)C#的类定义和语法更为简洁,支持如LINQ等高级特性。 2)C 提供更细粒度的控制,适用于系统编程和高性能需求。两者各有优势,选择应基于具体应用场景。

从XML转换到C 并进行数据操作可以通过以下步骤实现:1)使用tinyxml2库解析XML文件,2)将数据映射到C 的数据结构中,3)使用C 标准库如std::vector进行数据操作。通过这些步骤,可以高效地处理和操作从XML转换过来的数据。

C#使用自动垃圾回收机制,而C 采用手动内存管理。1.C#的垃圾回收器自动管理内存,减少内存泄漏风险,但可能导致性能下降。2.C 提供灵活的内存控制,适合需要精细管理的应用,但需谨慎处理以避免内存泄漏。

C 在现代编程中仍然具有重要相关性。1)高性能和硬件直接操作能力使其在游戏开发、嵌入式系统和高性能计算等领域占据首选地位。2)丰富的编程范式和现代特性如智能指针和模板编程增强了其灵活性和效率,尽管学习曲线陡峭,但其强大功能使其在今天的编程生态中依然重要。

C 学习者和开发者可以从StackOverflow、Reddit的r/cpp社区、Coursera和edX的课程、GitHub上的开源项目、专业咨询服务以及CppCon等会议中获得资源和支持。1.StackOverflow提供技术问题的解答;2.Reddit的r/cpp社区分享最新资讯;3.Coursera和edX提供正式的C 课程;4.GitHub上的开源项目如LLVM和Boost提升技能;5.专业咨询服务如JetBrains和Perforce提供技术支持;6.CppCon等会议有助于职业

C#适合需要高开发效率和跨平台支持的项目,而C 适用于需要高性能和底层控制的应用。1)C#简化开发,提供垃圾回收和丰富类库,适合企业级应用。2)C 允许直接内存操作,适用于游戏开发和高性能计算。

C 持续使用的理由包括其高性能、广泛应用和不断演进的特性。1)高效性能:通过直接操作内存和硬件,C 在系统编程和高性能计算中表现出色。2)广泛应用:在游戏开发、嵌入式系统等领域大放异彩。3)不断演进:自1983年发布以来,C 持续增加新特性,保持其竞争力。

C 和XML的未来发展趋势分别为:1)C 将通过C 20和C 23标准引入模块、概念和协程等新特性,提升编程效率和安全性;2)XML将继续在数据交换和配置文件中占据重要地位,但会面临JSON和YAML的挑战,并朝着更简洁和易解析的方向发展,如XMLSchema1.1和XPath3.1的改进。


热AI工具

Undresser.AI Undress
人工智能驱动的应用程序,用于创建逼真的裸体照片

AI Clothes Remover
用于从照片中去除衣服的在线人工智能工具。

Undress AI Tool
免费脱衣服图片

Clothoff.io
AI脱衣机

AI Hentai Generator
免费生成ai无尽的。

热门文章

热工具

EditPlus 中文破解版
体积小,语法高亮,不支持代码提示功能

WebStorm Mac版
好用的JavaScript开发工具

安全考试浏览器
Safe Exam Browser是一个安全的浏览器环境,用于安全地进行在线考试。该软件将任何计算机变成一个安全的工作站。它控制对任何实用工具的访问,并防止学生使用未经授权的资源。

SublimeText3 英文版
推荐:为Win版本,支持代码提示!

禅工作室 13.0.1
功能强大的PHP集成开发环境