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

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

Apr 04, 2025 am 08:06 AM
typescript

单元测试中的 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);
}
</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 方法。

结论

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

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

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

  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)

5个常见的JavaScript内存错误 5个常见的JavaScript内存错误 Aug 25, 2022 am 10:27 AM

JavaScript 不提供任何内存管理操作。相反,内存由 JavaScript VM 通过内存回收过程管理,该过程称为垃圾收集。

Vue3+TypeScript+Vite怎么使用require动态引入图片等静态资源 Vue3+TypeScript+Vite怎么使用require动态引入图片等静态资源 May 16, 2023 pm 08:40 PM

问题:Vue3+TypeScript+Vite的项目中如何使用require动态引入类似于图片等静态资源!描述:今天在开发项目时(项目框架为Vue3+TypeScript+Vite)需要动态引入静态资源,也就是img标签的src属性值为动态获取,按照以往的做法直接是require引入即可,如下代码:写上后代码波浪线报错,报错提示:找不到名称“require”。是否需要为节点安装类型定义?请尝试使用npmi--save-dev@types/node。ts(2580)在进行了npmi--save-d

如何使用MySQL在TypeScript中实现数据类型转换功能 如何使用MySQL在TypeScript中实现数据类型转换功能 Jul 29, 2023 pm 02:17 PM

如何使用MySQL在TypeScript中实现数据类型转换功能引言:在开发Web应用程序时,数据类型转换是一个非常常见的需求。在处理数据库中存储的数据时,特别是使用MySQL作为后端数据库时,我们经常需要将查询结果中的数据按照我们所需的类型进行转换。本文将介绍如何在TypeScript中利用MySQL实现数据类型转换的功能,并提供代码示例。一、准备工作:在开

如何使用Redis和TypeScript开发高性能计算功能 如何使用Redis和TypeScript开发高性能计算功能 Sep 20, 2023 am 11:21 AM

如何使用Redis和TypeScript开发高性能计算功能概述:Redis是一个开源的内存数据结构存储系统,具有高性能和可扩展性的特点。TypeScript是JavaScript的超集,提供了类型系统和更好的开发工具支持。结合Redis和TypeScript,我们可以开发出高效的计算功能来处理大数据集,并充分利用Redis的内存存储和计算能力。本文将介绍如何

Vue3中怎么使用TypeScript Vue3中怎么使用TypeScript May 13, 2023 pm 11:46 PM

如何声明字段名为枚举的类型?根据设计,type字段应该是一个枚举值,不应该由调用方随意设置。下面是Type的枚举声明,共有6个字段。enumType{primary="primary",success="success",warning="warning",warn="warn",//warningaliasdanger="danger",info="info",}TypeSc

Vue3相较于Vue2的变化:更好的 TypeScript 类型推导 Vue3相较于Vue2的变化:更好的 TypeScript 类型推导 Jul 07, 2023 pm 01:05 PM

Vue3相较于Vue2的变化:更好的TypeScript类型推导Vue是一种流行的JavaScript框架,用于构建用户界面。而Vue3是Vue框架的最新版本,在Vue2的基础上进行了大量改进和优化。其中之一是在TypeScript类型推导方面的提升。本文将介绍Vue3在类型推导方面的改进,并且通过代码示例进行说明。在Vue2中,我们需要手动为Vue组件

使用Redis和TypeScript开发可扩展的前端应用程序 使用Redis和TypeScript开发可扩展的前端应用程序 Aug 01, 2023 pm 09:21 PM

标题:使用Redis和TypeScript开发可扩展的前端应用程序引言:在当今互联网时代,可扩展性是任何应用程序的关键要素之一。前端应用程序也不例外。为了满足用户日益增长的需求,我们需要使用高效可靠的技术来构建可扩展的前端应用程序。在本文中,我们将介绍如何使用Redis和TypeScript来开发可扩展的前端应用程序,并通过代码示例演示其应用。Redis简介

在PHP中使用TypeScript编写更好的代码 在PHP中使用TypeScript编写更好的代码 Jun 19, 2023 pm 06:31 PM

随着JavaScript的不断发展,前端工程师们已经逐渐意识到JavaScript本身存在的一些问题,例如缺乏类型检查和模块化,这些问题在大型项目中经常会造成混乱和错误。为了解决这些问题,TypeScript应运而生,成为前端开发中越来越受欢迎的语言。而在后端开发领域中,PHP一直是一种极其流行的脚本语言。因此,结合TypeScript来开发PHP的应用程序

See all articles