首页 后端开发 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

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

热门文章

<🎜>:泡泡胶模拟器无穷大 - 如何获取和使用皇家钥匙
3 周前 By 尊渡假赌尊渡假赌尊渡假赌
北端:融合系统,解释
3 周前 By 尊渡假赌尊渡假赌尊渡假赌
Mandragora:巫婆树的耳语 - 如何解锁抓钩
3 周前 By 尊渡假赌尊渡假赌尊渡假赌

热工具

记事本++7.3.1

记事本++7.3.1

好用且免费的代码编辑器

SublimeText3汉化版

SublimeText3汉化版

中文版,非常好用

禅工作室 13.0.1

禅工作室 13.0.1

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

Dreamweaver CS6

Dreamweaver CS6

视觉化网页开发工具

SublimeText3 Mac版

SublimeText3 Mac版

神级代码编辑软件(SublimeText3)

热门话题

Java教程
1666
14
CakePHP 教程
1425
52
Laravel 教程
1327
25
PHP教程
1273
29
C# 教程
1252
24
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引入异步编程,未来将专注于开发者的生产力和云计算。

C#vs. C:学习曲线和开发人员的经验 C#vs. C:学习曲线和开发人员的经验 Apr 18, 2025 am 12:13 AM

C#和C 的学习曲线和开发者体验有显着差异。 1)C#的学习曲线较平缓,适合快速开发和企业级应用。 2)C 的学习曲线较陡峭,适用于高性能和低级控制的场景。

C社区:资源,支持和发展 C社区:资源,支持和发展 Apr 13, 2025 am 12:01 AM

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和XML:探索关系和支持 C和XML:探索关系和支持 Apr 21, 2025 am 12:02 AM

C 通过第三方库(如TinyXML、Pugixml、Xerces-C )与XML交互。1)使用库解析XML文件,将其转换为C 可处理的数据结构。2)生成XML时,将C 数据结构转换为XML格式。3)在实际应用中,XML常用于配置文件和数据交换,提升开发效率。

什么是C  中的静态分析? 什么是C 中的静态分析? Apr 28, 2025 pm 09:09 PM

静态分析在C 中的应用主要包括发现内存管理问题、检查代码逻辑错误和提高代码安全性。1)静态分析可以识别内存泄漏、双重释放和未初始化指针等问题。2)它能检测未使用变量、死代码和逻辑矛盾。3)静态分析工具如Coverity能发现缓冲区溢出、整数溢出和不安全API调用,提升代码安全性。

超越炒作:评估当今C的相关性 超越炒作:评估当今C的相关性 Apr 14, 2025 am 12:01 AM

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

C  中的chrono库如何使用? C 中的chrono库如何使用? Apr 28, 2025 pm 10:18 PM

使用C 中的chrono库可以让你更加精确地控制时间和时间间隔,让我们来探讨一下这个库的魅力所在吧。C 的chrono库是标准库的一部分,它提供了一种现代化的方式来处理时间和时间间隔。对于那些曾经饱受time.h和ctime折磨的程序员来说,chrono无疑是一个福音。它不仅提高了代码的可读性和可维护性,还提供了更高的精度和灵活性。让我们从基础开始,chrono库主要包括以下几个关键组件:std::chrono::system_clock:表示系统时钟,用于获取当前时间。std::chron

C的未来:改编和创新 C的未来:改编和创新 Apr 27, 2025 am 12:25 AM

C 的未来将专注于并行计算、安全性、模块化和AI/机器学习领域:1)并行计算将通过协程等特性得到增强;2)安全性将通过更严格的类型检查和内存管理机制提升;3)模块化将简化代码组织和编译;4)AI和机器学习将促使C 适应新需求,如数值计算和GPU编程支持。

See all articles