您的位置 首页 编程知识

单元测试中的 MockManager – 用于模拟的构建器模式

几年前我写过有关此的文章,但不太详细。这是同一想法的更精致的版本。 简介 单元测试对开发人员来说既是福也是祸。…

单元测试中的 MockManager - 用于模拟的构建器模式

几年前我写过有关此的文章,但不太详细。这是同一想法的更精致的版本。

简介

单元测试对开发人员来说既是福也是祸。它们允许快速测试功能、可读的使用示例、快速实验所涉及组件的场景。但它们也可能变得混乱,需要在每次代码更改时进行维护和更新,并且如果懒惰地完成,则无法隐藏错误而不是揭示错误。

我认为单元测试如此困难的原因是它与测试相关,而不是代码编写,而且单元测试的编写方式与我们编写的大多数其他代码相反。

在这篇文章中,我将为您提供一种编写单元测试的简单模式,该模式将增强所有好处,同时消除与正常代码的大部分认知失调。单元测试将保持可读性和灵活性,同时减少重复代码并且不添加额外的依赖项。

如何进行单元测试

但首先,让我们定义一个好的单元测试套件。

要正确测试一个类,必须以某种方式编写它。在这篇文章中,我们将介绍使用构造函数注入进行依赖项的类,这是我推荐的进行依赖项注入的方法。

然后,为了测试它,我们需要:

  • 涵盖积极的场景 – 当类执行其应该执行的操作时,使用设置和输入参数的各种组合来涵盖整个功能
  • 涵盖负面场景 – 当设置或输入参数错误时,类以正确的方式失败
  • 模拟所有外部依赖
  • 将所有测试设置、操作和断言保留在同一个测试中(通常称为 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); } 
登录后复制

让我们稍微打开一下它。例如,即使我们实际上并不关心记录器或缓存,我们也必须为每个构造函数依赖项声明一个模拟。在操作工厂的情况下,我们还必须设置一个返回另一个模拟的模拟方法。

在这个特定的测试中,我们主要编写了设置、一行 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); }  
登录后复制

拆包,没有提到缓存或记录器,因为我们不需要在那里进行任何设置。一切都已打包且可读。复制粘贴此内容并更改一些参数或某些行不再难看。在 arrange 中执行了三种方法,一种在 act 中执行,一种在 assert 中执行。仅抽象了实质的模拟细节:这里没有提及 moq 框架。事实上,无论决定使用哪种模拟框架,此测试看起来都是一样的。

让我们看一下 mockmanager 类。现在这会显得很复杂,但请记住,我们只编写一次并使用它很多次。该类的整体复杂性是为了使单元测试易于人类阅读,易于理解、更新和维护。

public class CalculatorMockManager {     private readonly Dictionary<OperationType,Mock<IOperation>> 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;     } } 
登录后复制

测试类所需的所有模拟都被声明为公共属性,允许对单元测试进行任何自定义。有一个 getservice 方法,它将始终返回被测试类的实例,并且所有依赖项都完全模拟。然后还有 with* 方法,它们自动设置各种场景并始终返回模拟管理器,以便可以链接它们。您还可以使用特定的断言方法,尽管在大多数情况下您会将一些输出与预期值进行比较,因此这些只是为了抽象出 moq 框架的verify 方法。

结论

此模式现在使测试编写与代码编写保持一致:

  • 抽象出任何上下文中你不关心的事物
  • 一次编写,多次使用
  • 人类可读的自记录代码
  • 低圈复杂度的小方法
  • 直观的代码编写

现在编写单元测试既简单又一致:

  1. 实例化您要测试的类的模拟管理器(或根据上述步骤编写一个)
  2. 为测试编写特定场景(自动完成现有已涵盖的场景步骤)
  3. 使用测试参数执行你想要测试的方法
  4. 检查一切是否符合预期

抽象并不止于模拟框架。相同的模式可以应用于每种编程语言!对于 或 javascript 或其他东西来说,模拟管理器构造将非常不同,但单元测试看起来几乎是一样的。

希望这有帮助!

以上就是单元测试中的 MockManager – 用于模拟的构建器模式的详细内容,更多请关注php中文网其它相关文章!

本文来自网络,不代表四平甲倪网络网站制作专家立场,转载请注明出处:http://www.elephantgpt.cn/4715.html

作者: nijia

发表回复

您的电子邮箱地址不会被公开。 必填项已用*标注

联系我们

联系我们

18844404989

在线咨询: QQ交谈

邮箱: 641522856@qq.com

工作时间:周一至周五,9:00-17:30,节假日休息

关注微信
微信扫一扫关注我们

微信扫一扫关注我们

关注微博
返回顶部