首页 后端开发 C++ 单元测试中的 MockManager - 用于模拟的构建器模式

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

Dec 19, 2024 pm 12:27 PM

MockManager in unit tests - a builder pattern used for mocks

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

简介

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

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

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

如何进行单元测试

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

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

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

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

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

希望这有帮助!

以上是单元测试中的 MockManager - 用于模拟的构建器模式的详细内容。更多信息请关注PHP中文网其他相关文章!

本站声明
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系admin@php.cn

热AI工具

Undresser.AI Undress

Undresser.AI Undress

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

AI Clothes Remover

AI Clothes Remover

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

Undress AI Tool

Undress AI Tool

免费脱衣服图片

Clothoff.io

Clothoff.io

AI脱衣机

Video Face Swap

Video Face Swap

使用我们完全免费的人工智能换脸工具轻松在任何视频中换脸!

热工具

记事本++7.3.1

记事本++7.3.1

好用且免费的代码编辑器

SublimeText3汉化版

SublimeText3汉化版

中文版,非常好用

禅工作室 13.0.1

禅工作室 13.0.1

功能强大的PHP集成开发环境

Dreamweaver CS6

Dreamweaver CS6

视觉化网页开发工具

SublimeText3 Mac版

SublimeText3 Mac版

神级代码编辑软件(SublimeText3)

C语言数据结构:树和图的数据表示与操作 C语言数据结构:树和图的数据表示与操作 Apr 04, 2025 am 11:18 AM

C语言数据结构:树和图的数据表示与操作树是一个层次结构的数据结构由节点组成,每个节点包含一个数据元素和指向其子节点的指针二叉树是一种特殊类型的树,其中每个节点最多有两个子节点数据表示structTreeNode{intdata;structTreeNode*left;structTreeNode*right;};操作创建树遍历树(先序、中序、后序)搜索树插入节点删除节点图是一个集合的数据结构,其中的元素是顶点,它们通过边连接在一起边可以是带权或无权的数据表示邻

C语言文件操作难题的幕后真相 C语言文件操作难题的幕后真相 Apr 04, 2025 am 11:24 AM

文件操作难题的真相:文件打开失败:权限不足、路径错误、文件被占用。数据写入失败:缓冲区已满、文件不可写、磁盘空间不足。其他常见问题:文件遍历缓慢、文本文件编码不正确、二进制文件读取错误。

c语言函数的基本要求有哪些 c语言函数的基本要求有哪些 Apr 03, 2025 pm 10:06 PM

C语言函数是代码模块化和程序搭建的基础。它们由声明(函数头)和定义(函数体)组成。C语言默认使用值传递参数,但也可使用地址传递修改外部变量。函数可以有返回值或无返回值,返回值类型必须与声明一致。函数命名应清晰易懂,使用驼峰或下划线命名法。遵循单一职责原则,保持函数简洁性,以提高可维护性和可读性。

c语言函数名定义 c语言函数名定义 Apr 03, 2025 pm 10:03 PM

C语言函数名定义包括:返回值类型、函数名、参数列表和函数体。函数名应清晰、简洁、统一风格,避免与关键字冲突。函数名具有作用域,可在声明后使用。函数指针允许将函数作为参数传递或赋值。常见错误包括命名冲突、参数类型不匹配和未声明的函数。性能优化重点在函数设计和实现上,而清晰、易读的代码至关重要。

c语言函数的概念 c语言函数的概念 Apr 03, 2025 pm 10:09 PM

C语言函数是可重复利用的代码块,它接收输入,执行操作,返回结果,可将代码模块化提高可复用性,降低复杂度。函数内部机制包含参数传递、函数执行、返回值,整个过程涉及优化如函数内联。编写好的函数遵循单一职责原则、参数数量少、命名规范、错误处理。指针与函数结合能实现更强大的功能,如修改外部变量值。函数指针将函数作为参数传递或存储地址,用于实现动态调用函数。理解函数特性和技巧是编写高效、可维护、易理解的C语言程序的关键。

c上标3下标5怎么算 c上标3下标5算法教程 c上标3下标5怎么算 c上标3下标5算法教程 Apr 03, 2025 pm 10:33 PM

C35 的计算本质上是组合数学,代表从 5 个元素中选择 3 个的组合数,其计算公式为 C53 = 5! / (3! * 2!),可通过循环避免直接计算阶乘以提高效率和避免溢出。另外,理解组合的本质和掌握高效的计算方法对于解决概率统计、密码学、算法设计等领域的许多问题至关重要。

CS-第 3 周 CS-第 3 周 Apr 04, 2025 am 06:06 AM

算法是解决问题的指令集,其执行速度和内存占用各不相同。编程中,许多算法都基于数据搜索和排序。本文将介绍几种数据检索和排序算法。线性搜索假设有一个数组[20,500,10,5,100,1,50],需要查找数字50。线性搜索算法会逐个检查数组中的每个元素,直到找到目标值或遍历完整个数组。算法流程图如下:线性搜索的伪代码如下:检查每个元素:如果找到目标值:返回true返回falseC语言实现:#include#includeintmain(void){i

C#与C:历史,进化和未来前景 C#与C:历史,进化和未来前景 Apr 19, 2025 am 12:07 AM

C#和C 的历史与演变各有特色,未来前景也不同。1.C 由BjarneStroustrup在1983年发明,旨在将面向对象编程引入C语言,其演变历程包括多次标准化,如C 11引入auto关键字和lambda表达式,C 20引入概念和协程,未来将专注于性能和系统级编程。2.C#由微软在2000年发布,结合C 和Java的优点,其演变注重简洁性和生产力,如C#2.0引入泛型,C#5.0引入异步编程,未来将专注于开发者的生产力和云计算。

See all articles