0

0

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

花韻仙語

花韻仙語

发布时间:2024-12-12 20:21:25

|

1105人浏览过

|

来源于dev.to

转载

单元测试中的 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 转换为标记。别担心,这不是一个真正的课程,只是一个例子。让我们看一个“传统”测试:

HTShop网上购物系统
HTShop网上购物系统

HTShop网上购物系统由恒天网络科技有限公司根据国际先进技术和国内商务特点自主版权开发的一款具有强大功能的B2C电子商务网上购物平台。HTShop以国际上通用流行的B/S(浏览器/服务器)模式进行设计,采用微软公司的ASP.NET(C#)技术构建而成。 2007-11-10 HTShop CS 通用标准版 v1.1.11.10 更新内容自由更换模版功能开放 修改了购买多款商品,会员中心订单只显示

下载
[testmethod]
public void calculate_additionworks()
{
    // arrange
    var tokenparsermock = new mock();
    tokenparsermock
        .setup(m => m.parse(it.isany()))
        .returns(
            new list {
                calculatortoken.addition, calculatortoken.from(1), calculatortoken.from(1)
            }
        );

    var mathoperationfactorymock = new mock();

    var operationmock = new mock();
    operationmock
        .setup(m => m.execute(1, 1))
        .returns(2);

    mathoperationfactorymock
        .setup(m => m.getoperation(operationtype.add))
        .returns(operationmock.object);

    var cachemock = new mock();
    var loggermock = new mock();

    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.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> operationMocks = new();

    public Mock TokenParserMock { get; } = new();
    public Mock MathOperationFactoryMock { get; } = new();
    public Mock CacheMock { get; } = new();
    public Mock LoggerMock { get; } = new();

    public CalculatorMockManager WithParsedTokens(List tokens)
    {
        TokenParserMock
            .Setup(m => m.Parse(It.IsAny()))
            .Returns(
                new List {
                    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();
        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)
    {
        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 或其他东西来说,模拟管理器构造将非常不同,但单元测试看起来几乎是一样的。

希望这有帮助!

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

热门AI工具

更多
DeepSeek
DeepSeek

幻方量化公司旗下的开源大模型平台

豆包大模型
豆包大模型

字节跳动自主研发的一系列大型语言模型

通义千问
通义千问

阿里巴巴推出的全能AI助手

腾讯元宝
腾讯元宝

腾讯混元平台推出的AI助手

文心一言
文心一言

文心一言是百度开发的AI聊天机器人,通过对话可以生成各种形式的内容。

讯飞写作
讯飞写作

基于讯飞星火大模型的AI写作工具,可以快速生成新闻稿件、品宣文案、工作总结、心得体会等各种文文稿

即梦AI
即梦AI

一站式AI创作平台,免费AI图片和视频生成。

ChatGPT
ChatGPT

最最强大的AI聊天机器人程序,ChatGPT不单是聊天机器人,还能进行撰写邮件、视频脚本、文案、翻译、代码等任务。

相关专题

更多
js 字符串转数组
js 字符串转数组

js字符串转数组的方法:1、使用“split()”方法;2、使用“Array.from()”方法;3、使用for循环遍历;4、使用“Array.split()”方法。本专题为大家提供js字符串转数组的相关的文章、下载、课程内容,供大家免费下载体验。

320

2023.08.03

js截取字符串的方法
js截取字符串的方法

js截取字符串的方法有substring()方法、substr()方法、slice()方法、split()方法和slice()方法。本专题为大家提供字符串相关的文章、下载、课程内容,供大家免费下载体验。

212

2023.09.04

java基础知识汇总
java基础知识汇总

java基础知识有Java的历史和特点、Java的开发环境、Java的基本数据类型、变量和常量、运算符和表达式、控制语句、数组和字符串等等知识点。想要知道更多关于java基础知识的朋友,请阅读本专题下面的的有关文章,欢迎大家来php中文网学习。

1503

2023.10.24

字符串介绍
字符串介绍

字符串是一种数据类型,它可以是任何文本,包括字母、数字、符号等。字符串可以由不同的字符组成,例如空格、标点符号、数字等。在编程中,字符串通常用引号括起来,如单引号、双引号或反引号。想了解更多字符串的相关内容,可以阅读本专题下面的文章。

625

2023.11.24

java读取文件转成字符串的方法
java读取文件转成字符串的方法

Java8引入了新的文件I/O API,使用java.nio.file.Files类读取文件内容更加方便。对于较旧版本的Java,可以使用java.io.FileReader和java.io.BufferedReader来读取文件。在这些方法中,你需要将文件路径替换为你的实际文件路径,并且可能需要处理可能的IOException异常。想了解更多java的相关内容,可以阅读本专题下面的文章。

655

2024.03.22

php中定义字符串的方式
php中定义字符串的方式

php中定义字符串的方式:单引号;双引号;heredoc语法等等。想了解更多字符串的相关内容,可以阅读本专题下面的文章。

610

2024.04.29

go语言字符串相关教程
go语言字符串相关教程

本专题整合了go语言字符串相关教程,阅读专题下面的文章了解更多详细内容。

172

2025.07.29

c++字符串相关教程
c++字符串相关教程

本专题整合了c++字符串相关教程,阅读专题下面的文章了解更多详细内容。

83

2025.08.07

C++ 设计模式与软件架构
C++ 设计模式与软件架构

本专题深入讲解 C++ 中的常见设计模式与架构优化,包括单例模式、工厂模式、观察者模式、策略模式、命令模式等,结合实际案例展示如何在 C++ 项目中应用这些模式提升代码可维护性与扩展性。通过案例分析,帮助开发者掌握 如何运用设计模式构建高质量的软件架构,提升系统的灵活性与可扩展性。

14

2026.01.30

热门下载

更多
网站特效
/
网站源码
/
网站素材
/
前端模板

精品课程

更多
相关推荐
/
热门推荐
/
最新课程
TypeScript 教程
TypeScript 教程

共19课时 | 2.6万人学习

TypeScript——十天技能课堂
TypeScript——十天技能课堂

共21课时 | 1.1万人学习

TypeScript-45分钟入门
TypeScript-45分钟入门

共6课时 | 0.5万人学习

关于我们 免责申明 举报中心 意见反馈 讲师合作 广告合作 最新更新
php中文网:公益在线php培训,帮助PHP学习者快速成长!
关注服务号 技术交流群
PHP中文网订阅号
每天精选资源文章推送

Copyright 2014-2026 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号