【The Art of Unit Testing 3_自学笔记06】3.4 + 3.5 单元测试核心技能之:函数式注入与模块化注入的解决方案简介

文章目录

    • 3.4 函数式依赖注入技术 Functional injection techniques
    • 3.5 模块化依赖注入技术 Modular injection techniques

写在前面
上一篇的最后部分对第三章后续内容做了一个概括性的梳理,并给出了断开依赖项的最简单的实现方案,函数参数值注入法。本篇接着介绍函数式注入与模块化注入的具体实现。窃以为后者是本章的难点,需要用心体会作者的设计思路。

(接 上篇 3.3 小节)

3.4 函数式依赖注入技术 Functional injection techniques

函数式实现(FP)与面向对象实现(OOP)并无绝对的优劣之分。FP 固然简洁、清晰、自证性强,但学习曲线陡峭也是不争的事实。

上一节讲到断开外部依赖的一种方案——参数注入法。它通过重构原函数,使其接收一个新参数值(即人为控制的星期索引值)。但这里的参数除了基本类型外,还可以将具体星期值的计算逻辑封装到一个函数内,然后将该函数以参数的形式注入原函数。

于是有了函数式注入的第一套方案——函数作参数注入。对原函数模块 password-verifier-time00.js 作如下更改(L2、L3):

const SUNDAY = 0, SATURDAY = 6;
const verifyPassword3 = (input, rules, getDayFn) => {const dayOfWeek = getDayFn();if ([SATURDAY, SUNDAY].includes(dayOfWeek)) {throw Error("It's the weekend!");}// more code goes here...// return list of errors found..return [];
};

于是单元测试 password-verifier-time00.spec.js 相应变为(L4、L5,以及 L9、L11):

const SUNDAY = 0, SATURDAY = 6, MONDAY = 2;
describe('verifier3 - dummy function', () => {test('on weekends, throws exceptions', () => {const alwaysSunday = () => SUNDAY;expect(() => verifyPassword3('anything', [], alwaysSunday)).toThrowError("It's the weekend!");});test('on week days, works fine', () => {const alwaysMonday = () => MONDAY;const result = verifyPassword3('anything', [], alwaysMonday);expect(result.length).toBe(0);});
});

实测结果:

图 5 改为函数作参数后的实测结果

【图 5 改为函数作参数后的实测结果】

再进一步,可将传入的函数改造为一个高阶函数(high order function,简称 HOF),让依赖注入逻辑与密码校验逻辑分开。这样就有了书中所说的 工厂函数(factory functions) 方案。此时原函数已经被完全改造了。

password-verifier-time00.js

const SUNDAY = 0, SATURDAY = 6;const makeVerifier = (rules, dayOfWeekFn) => {return function (input) {if ([SATURDAY, SUNDAY].includes(dayOfWeekFn())) {throw new Error("It's the weekend!");}const errors = [];// more code goes here..return errors;};
};module.exports = {makeVerifier
};

于是单元测试 password-verifier-time00.spec.js 也要同步更新:

const { makeVerifier } = require('../password-verifier-time00');
const SUNDAY = 0, MONDAY = 1;describe('verifier3 - dummy function', () => {test('factory method: on weekends, throws exceptions', () => {const alwaysSunday = () => SUNDAY;const verifyPassword = makeVerifier([], alwaysSunday);expect(() => verifyPassword('anything')).toThrow("It's the weekend!");});test('on week days, works fine', () => {const alwaysMonday = () => MONDAY;const verifyPassword = makeVerifier([], alwaysMonday);const result = verifyPassword('anything');expect(result.length).toBe(0);});});

实测结果同上面的 图 5。这样做的好处,就是让校验的配置独立于校验的执行,在减少原函数参数个数的同时,测试用例的可读性也更强。一举多得。

3.5 模块化依赖注入技术 Modular injection techniques

这一节开始加大难度了,主要目的在于让大家感受一下模块化注入的繁琐。为什么会这么繁琐呢?因为以模块的方式注入依赖项虽然写起来很爽,但对于单元测试而言完全是另一码事。回到最开始的原函数版本——

password-verifier-time00.js

const moment = require("moment");
const SUNDAY = 0, SATURDAY = 6;const verifyPassword = (input, rules) => {const dayOfWeek = moment().day();if ([SATURDAY, SUNDAY].includes(dayOfWeek)) {throw Error("It's the weekend!");}// more code goes here...// return list of errors found..return [];
};module.exports = {verifyPassword,
};

怎样从单元测试的角度断开上述代码中的直接依赖呢?答案是没有现成的方法,只能“曲线救国”。这就要用到 3.2 节补充的 Seam 缝隙的概念了:通过构造一个特定的写法,以便将直接依赖项替换成单元测试能够直接干预的代码,实现 控制反转

以下代码给出了一个示例版本:

核心重构1:根据模块化注入方案重构的新版待测函数示例

const originalDependencies = {moment: require('moment')
};let dependencies = { ...originalDependencies };const inject = (fakes) => {Object.assign(dependencies, fakes);return function reset () {dependencies = { ...originalDependencies };};
};const SUNDAY = 0; const SATURDAY = 6;const verifyPassword = (input, rules) => {const dayOfWeek = dependencies.moment().day();if ([SATURDAY, SUNDAY].includes(dayOfWeek)) {throw Error("It's the weekend!");}// more code goes here...// return list of errors found..return [];
};module.exports = {SATURDAY,verifyPassword,inject
};

相比其他小节,上述代码是本章最难的一段。原因很简单——我之前从没这样认真研究过(​开个玩笑​ 😆)。

先来仔细看看这段代码。为了成功断开由 moment.js 引入的直接依赖,需要构造一个新的写法,即第 17 行中的星期值生成逻辑:

// 改造前:
const dayOfWeek = moment().day();
// 改造后:
const dayOfWeek = dependencies.moment().day();

先甭管 dependencies 怎么定义的,写成第 4 行的形式后,最初的 moment().day() 就变成了 dependencies 下的 moment() 方法;这时,只要再设计一个注入逻辑(比如第 7 至 12 行的 inject(fakes) 函数),并让它在运行测试时对 dependencies.moment 属性 重新赋值,这样就实现了原始依赖 moment 模块的平替,从而实现 控制反转;最后,为了不破坏原函数逻辑,等到单元测试结束,还得再设计一套 重置逻辑,让 dependencies.moment 重新指向 moment 模块。这就是模块化注入的大致流程。

与之配套的单元测试代码如下:

核心代码2:模块化改造后的新校验函数在单元测试模块中的应用示例

const { inject, verifyPassword, SATURDAY } = require('../password-verifier-time00');const injectDate = (newDay) => {const reset = inject({moment: function () {// we're faking the moment.js module's API here.return {day: () => newDay};}});return reset;
};describe('verifyPassword', () => {describe('when its the weekend', () => {it('throws an error', () => {const reset = injectDate(SATURDAY);expect(() => verifyPassword('any input')).toThrowError("It's the weekend!");reset();});});
});

第一次看这两段代码,头是真的晕。比如上面的高阶函数 injectDate():它接收一个普通的星期值 newDay,然后用这个值构造了一个测试专用的伪对象 fakes(示例中没有单独声明)并直接传入 inject() 方法,最后将执行结果——即包含重置逻辑的 reset() 函数——作为函数结果返回。最后在测试用例的第 18 行和第 23 行实现了控制的反转与依赖项的重置。

抱着将信将疑的心理,我在本地实测了上述代码,居然真的可以这样写:

图 6 按照模块化注入方案重构原函数得到的实测结果

【图 6 按照模块化注入方案重构原函数得到的实测结果】

正当我惊叹于作者对 JavaScript 闭包的深入理解时,大佬又再次复盘上述写法,对比了该方案的优劣:

  • 优势:解决了最开始的直接依赖问题,使用时也相对比较简单(按大佬的说法,多写几遍自然就有感觉了……);
  • 劣势:即闭包 dependencies 中的 moment 属性与依赖的 moment 模块之间未能实现解耦。遇到真实项目测试就傻眼了:成千上万个依赖项接口难不成还得挨个重构成特定的闭包属性?

为此,作者给出了如下建议:

  1. 永远不要在代码中直接使用第三方依赖,最好加一个适配层缓冲一下,这样就不怕第三方库修改接口或者更换其他依赖项了。
  2. 慎用这个天坑的模块注入方案,换成其他实现方案,比如之前介绍的视函数为参数、或者函数柯里化;或者后面紧接着会介绍的 构造函数 以及 接口 的解决方案。

总之,这一节主要是给后续的高级方案做铺垫用的;对我而言也是增长见识的一节,让我知道设计模式中的适配器模式在单元测试中原来还能这么用。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/463565.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

电磁兼容(EMC):整改案例(六)Y电容过大导致雷击浪涌炸机

目录 1. 异常现象 2. 原因分析 3. 整改方案 4. 总结 1. 异常现象 某金属外壳带接地线的产品按GB/T 17626.5进行雷击浪涌测试,在L,N线对PE进行4kV浪涌电压测试时,出现炸机现象,AC-DC电源芯片损坏。而在L,N线间进行2…

代码之眼,陈欣的xml解密之路

第一章 在未来的世界里,科技已经发展到了令人难以想象的地步。人工智能、量子计算和生物技术交织在一起,创造了一个全新的社会形态。在这个世界中,有一个名为“代码守护者”的组织,专门负责维护全球信息系统的安全和稳定。 陈欣是…

L0G1000:Linux+InternStudio 闯关作业

1. 配置基础环境 首先,打开 Intern Studio 界面,点击 创建开发机 配置开发机系统。 InternStudio 填写 开发机名称 后,点击 选择镜像 使用 Cuda11.7-conda 镜像,然后在资源配置中,使用 10% A100 * 1 的选项&#xff…

爬虫笔记22——当当网图书详情页静、动态数据爬取

当当网动态数据爬取 静态数据爬取动态数据爬取接口参数的获取 静态数据爬取 进入图书详情,这里的图书数据信息比如标题、价格、图片都是非结构化数据,可以使用xpath语法提取。是很简单的数据采集了,就不细说了。 动态数据爬取 滑到下面这里的…

使用pathview在线渲染KEGG Pathway Map,给感兴趣的基因、化合物添加颜色

导读: 通过将用户提供的基因表达定量数据,化合物定量数据映射并渲染到相关的KEGG通路图上,能够帮助我们直观且系统地研究基因、酶、化合物间的关系。 KEGG通路图简介 KEGG PATHWAY数据库是一系列手动绘制的图形图谱的集合,称为…

自动化测试工具Ranorex Studio(二十一)-适配一个已存在的对象库

通过录制一个手工测试场景我们创建了一个对象库。录制期间用到的每个UI元素都在库中创建了一个新的条目。默认情况下,一个新的Ranorex Studio项目包含一个库文件(*.rxrep),这个文件可以被多个录制模块或代码模块使用。 图:一个库的文件视图…

OpenSLL下载,环境变量配置

https://slproweb.com/products/Win32OpenSSL.html 环境变量 新建一个path为安装选择的目录的bin路径

【MyBatis】【基于轻量型架构的WEB开发】课程 课后习题 章节测试

mybatis关联查询、缓存、注解 一. 单选题 1. 下列关于 <collection> 元素的描述正确的是&#xff08;&#xff09;。 A. MyBatis 就是通过 <collection> 元素来处理一对多关联关系的 B. <collection> 元素的属性与 <association> 元素完全相同 C.…

JavaEE-多线程上

文章目录 线程概述进程/线程多线程的作用JVM关于线程资源的规范关于Java程序的运行原理 并发与并行并发(concurrency)并行(parallellism)并发编程与并行编程 线程的调度策略分时调度模型抢占式调度模型 创建线程线程类分析入门实现线程的第一种方式实现线程的第二种方式 线程的…

SQL 常用语句

目录 我的测试环境 学习文档 进入数据库 基础通关测验 语句-- 查 展示数据库&#xff1b; 进入某个数据库&#xff1b; 展示表&#xff1a; 展示某个表 desc 查询整个表&#xff1a; 查询特定列&#xff1a; 范围查询 等于特定值 不等于 介于 特定字符查询 Li…

[MySQL]DQL语句(一)

查询语句是数据库操作中最为重要的一系列语法。查询关键字有 select、where、group、having、order by、imit。其中imit是MySQL的方言&#xff0c;只在MySQL适用。 数据库查询又分单表查询和多表查询&#xff0c;这里讲一下单表查询。 基础查询 # 查询指定列 SELECT * FROM …

【Unity】鼠标点击获取世界坐标位置:物体移动至鼠标点击的位置

需求说明 鼠标点击3D场景时&#xff0c;可以获取其所在的世界坐标&#xff1b; 鼠标点击3D物体时&#xff0c;可以获取该物体&#xff1b; 鼠标点击3D物体时&#xff0c;可以让玩家移动至该物体&#xff1b; 成果展示 Scene部分 关于仓库栏的设置&#xff0c;物体如何进入…

使用nvm切换node版本失败

​ 使用nvm切换node版本失败&#xff08;原node版本v20.14.0&#xff0c;我使用nvm use 16.9.1切换node版本后&#xff0c;显示Now using node v16.9.1可当我使用命令node -v查看当前node版本时还是v20.14.0&#xff0c;意味着版本切换失败&#xff09;&#xff1a; 这个原因大…

Hive数据库操作语法

数据类型 内部表和外部表 内部表 &#xff08;CREATE TABLE table_name ......&#xff09;未被external关键字修饰的即是内部表&#xff0c; 即普通表。 内部表又称管理表,内部表数据存储的位置由hive.metastore.warehouse.dir参数决定&#xff08;默认&#xff1a;/user/h…

【Python TensorFlow】入门到精通

TensorFlow 是一个开源的机器学习框架&#xff0c;由 Google 开发&#xff0c;广泛应用于机器学习和深度学习领域。本篇将详细介绍 TensorFlow 的基础知识&#xff0c;并通过一系列示例来帮助读者从入门到精通 TensorFlow 的使用。 1. TensorFlow 简介 1.1 什么是 TensorFlow…

设计模式08-行为型模式1(命令模式/迭代器模式/观察者模式/Java)

五、行为型模式 **行为模式的定义&#xff1a;**行为型模式是对不同的对象之间划分职责和算法的抽象化。行为型模式定义了系统中对象之间的交互与通信&#xff0c;研究系统在运行时对象之间的相互通信与协作&#xff0c;进一步明确对象的职责&#xff0c;包括对系统中较为复杂的…

服务器作业2

关闭防火墙 [rootlocalhost ~]# systemctl stop firewalld [rootlocalhost ~]# setenforce 0 配置文件 创建用户nfs-upload [rootlocalhost ~]# useradd -u 210 nfs-upload [rootlocalhost ~]# groupmod -g 210 nfs-upload 创建tom用户 [rootlocalhost ~]# useradd tom 查看to…

【数据结构】堆:建堆/向下调整/上向调整/堆排序/TOK问题

文章目录 前言堆的定义1.大小堆2.完全二叉树 堆的实现堆的数据结构初始化销毁取堆顶元素判断堆是否为空父结点和子结点下标关系&#xff08;重要&#xff09; 向下调整法-O(n)小堆版大堆版 向上调整法-nlog(n)堆的插入和删除插入(调用向上调整)删除(调用向下调整) 构建最大堆向…

java学习1

一、运算符 1.算术运算符 在代码中&#xff0c;如果有小数参与计算&#xff0c;结果有可能不精确 1-1.隐式转换和强制转换 数字进行运算时&#xff0c;数据类型不一样不能运算&#xff0c;需要转成一样的&#xff0c;才能运算 &#xff08;1&#xff09;隐式转换&#xff1a…

20.体育馆使用预约系统(基于springboot和vue的Java项目)

目录 1.系统的受众说明 2.开发环境与技术 2.1 Java语言 2.2 MYSQL数据库 2.3 IDEA开发工具 2.4 Spring Boot框架 3.需求分析 3.1 可行性分析 3.1.1 技术可行性 3.1.2 经济可行性 3.1.3 操作可行性 3.2 系统流程分析 3.3 系统性能需求 3.4 系统功能需求 4.系…