【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 改为函数作参数后的实测结果】
再进一步,可将传入的函数改造为一个高阶函数(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 按照模块化注入方案重构原函数得到的实测结果】
正当我惊叹于作者对 JavaScript 闭包的深入理解时,大佬又再次复盘上述写法,对比了该方案的优劣:
- 优势:解决了最开始的直接依赖问题,使用时也相对比较简单(按大佬的说法,多写几遍自然就有感觉了……);
- 劣势:即闭包
dependencies中的moment属性与依赖的moment模块之间未能实现解耦。遇到真实项目测试就傻眼了:成千上万个依赖项接口难不成还得挨个重构成特定的闭包属性?
为此,作者给出了如下建议:
- 永远不要在代码中直接使用第三方依赖,最好加一个适配层缓冲一下,这样就不怕第三方库修改接口或者更换其他依赖项了。
- 慎用这个天坑的模块注入方案,换成其他实现方案,比如之前介绍的视函数为参数、或者函数柯里化;或者后面紧接着会介绍的 构造函数 以及 接口 的解决方案。
总之,这一节主要是给后续的高级方案做铺垫用的;对我而言也是增长见识的一节,让我知道设计模式中的适配器模式在单元测试中原来还能这么用。
相关文章:
【The Art of Unit Testing 3_自学笔记06】3.4 + 3.5 单元测试核心技能之:函数式注入与模块化注入的解决方案简介
文章目录 3.4 函数式依赖注入技术 Functional injection techniques3.5 模块化依赖注入技术 Modular injection techniques 写在前面 上一篇的最后部分对第三章后续内容做了一个概括性的梳理,并给出了断开依赖项的最简单的实现方案,函数参数值注入法。本…...
【VSCode】配置
安装插件 C vscode-icons gdb调试 https://www.bilibili.com/video/BV15U4y1x7b2/?spm_id_from333.999.0.0&vd_sourcedf0ce73d9b9b61e6d4771898f1441f7f https://www.bilibili.com/video/BV1pU4y1W74Z?spm_id_from333.788.recommend_more_video.-1&vd_sourcedf0…...
Linux 常用命令整理大全及命令使用心得
本文章是为了总结自己用过的命令,以及一些心得,网上有很多类似的,但自己总结才能更好的理解。 文章目录 一、文件和目录管理01、 ls :列出目录内容02、cd:更改当前目录03、pwd:显示当前工作目录04、mkdir&a…...
计算器的实现
计算器的⼀般实现 计算器的一般实现:优化:使⽤函数指针数组的实现: 计算器的一般实现: #include <stdio.h> int add(int a, int b) {return a b; } int sub(int a, int b) {return a - b; } int mul(int a, int b) {retur…...
这个工具帮你快速实现数据集成和同步
在这个信息爆炸的时代,数据的流动和同步逐渐成为企业运营的命脉。然而,企业正面临着前所未有的数据挑战,无论是跨地域的分公司协作,还是云服务与本地数据库的交互,数据的集成、清洗、转换和加载(ETL&#x…...
论文阅读:Computational Long Exposure Mobile Photography (一)
这篇文章是谷歌发表在 2023 ACM transaction on Graphic 上的一篇文章,介绍如何在手机摄影中实现长曝光的一些拍摄效果。 Abstract 长曝光摄影能拍出令人惊叹的影像,用运动模糊来呈现场景中的移动元素。它通常有两种模式,分别产生前景模糊或…...
项目解决方案:多地连锁药店高清视频监控系统建设解决方案(设计方案)
目录 一.项目背景 1.1背景描述 1.2需求分析 二.设计依据和建设目标 2.1设计依据 2.2建设目标 三.系统设计实现 3.1系统方案设计 3.2网络组网说明 四.建设系统特色 4.1安全性 4.2节约建设成本 4.3原有资源的再利用 4.4可扩展性 五.产品介绍 5.1概述 5.2设备…...
utf-8、pbkdf2_sha
#utf-8加密、解密 import base64 base64.b64encode(lienlien123.encode(utf-8)) bbGllbmxpZW4xMjM base64.b64decode(bbGllbmxpZW4xMjM.decode(utf-8)) blienlien123 #pbkdf2_sha加密,校验 # 该种密码在不同时刻会有产生不同的加密结果 # 该加密方法使用的是散列…...
Java之包,抽象类,接口
目录 包 导入包 静态导入 将类放入包 常见的系统包 抽象类 语法规则 注意事项: 抽象类的作用 接口 实现多个接口 接口间的继承 接口使用实例 (法一)实现Comparable接口的compareTo()方法 (法二)实现Comp…...
HarmonyOS鸿蒙开发入门,常用ArkUI组件学习(二)
书接上回,让我们继续来学习ArkUI的其他组件 目录,可以点击跳转到想要了解的组件详细内容 组件四:Button组件五:Slider组件六: Column & Row组件七:循环控制组件八: List 组件四:…...
斩!JavaScript语法进阶
一、DOM 概述 DOM 是 JavaScript 操作网页的接口,全称为“文档对象模型”(Document Object Model)。当网页被加载时,浏览器将网页转为一个DOM,并用JS进行各种操作。比如:改变页面中的HTML 元素及其属性&am…...
UFO:Windows操作系统的具象智能代理
近年来,随着AI技术的发展,智能代理在各种应用中扮演着越来越重要的角色。微软推出的UFO(User-Focused Operator)正是这样一个出色的多代理框架,旨在通过无缝导航和操作,满足用户在Windows操作系统中跨多个应…...
win10/11无休眠设置和断电后电池模式自动休眠而不是睡眠-用以省电
1、打开休眠设置选项 打开控制面板\所有控制面板项\电源选项\ 左侧的选择电源按钮的功能 默认状态没有休眠 1、管理员权限打开cmd或者power shell 2、输入一下指令,打开休眠选项 powercfg -hibernate on关闭后重新打开 控制面板\所有控制面板项\电源选项\左侧的选…...
【动态规划之斐波那契数列模型】——累加递推型动态规划
文章目录 第N个泰波那契数列面试题08.01.三步问题使用最小花费爬楼梯解码问题 第N个泰波那契数列 解题思路: 泰波那契数列的第 N 项定义为前面三项之和,即 T0 0, T1 1, T2 1,从 T3 开始,每一项都等于前三项的和。要找到第 N 项…...
5g通信系统用到的crc码
5g通信系统用到的crc码 关注 在5G通信系统中,CRC码(循环冗余校验码)扮演着关键角色,它通过执行多项式除法运算来检测数据在传输过程中是否发生错误。5G通信系统中采用了多种CRC码,每种码都有其独特的计算方法和校验特…...
Ubuntu-22.04 虚拟机安装
1. Ubuntu安装方式 1.1. 基于物理介质安装 光盘安装:通过将 Ubuntu 镜像刻录到光盘,在计算机 BIOS/UEFI 中设置光盘为第一启动项,然后按照安装程序的提示进行语言选择、分区、用户信息设置等操作来完成安装。这种方式需要有光盘刻录设备和空…...
Windows、Linux系统上进行CPU和内存压力测试
CPU和内存压力测试 1. Linux环境 Linux环境下,我们可以用 stress 工具进行内存、CPU等的压力测试。 【1】. stress工具说明 [kalamikysrv1 ~]$ stress --help stress imposes certain types of compute stress on your systemUsage: stress [OPTION [ARG]] ...-…...
FFmpeg 4.3 音视频-多路H265监控录放C++开发八,使用SDLVSQT显示yuv文件 ,使用ffmpeg的AVFrame
一. AVFrame 核心回顾,uint8_t *data[AV_NUM_DATA_POINTERS] 和 int linesize[AV_NUM_DATA_POINTERS] AVFrame 存储的是解码后的数据,(包括音频和视频)例如:yuv数据,或者pcm数据,参考AVFrame结…...
HTML 标签属性——<a>、<img>、<form>、<input>、<table> 标签属性详解
文章目录 1. `<a>`元素属性hreftargetname2. `<img>`元素属性srcaltwidth 和 height3. `<form>`元素属性actionmethodenctype4. `<input>`元素属性typevaluenamereadonly5. `<table>`元素属性cellpaddingcellspacing小结HTML元素除了可以使用全局…...
css简写属性
一些属性,如 font、background、padding、border 和 margin 等属性称为简写属性。它们允许在一行中设置多个属性值,从而节省时间并使代码更整洁。 /* 在像 padding 和 margin 这样的 4 值简写语法中,数值的应用顺序是上、右、下、左ÿ…...
避坑指南:电商评论情感分析中常见的5大误区与解决方案
避坑指南:电商评论情感分析中常见的5大误区与解决方案 当你在深夜盯着屏幕上一堆杂乱无章的电商评论数据时,是否曾怀疑过自己的情感分析模型在"说谎"?那些看似完美的准确率数字背后,可能隐藏着连老手都会踩中的陷阱。本…...
LabVIEW新手避坑指南:用For循环和数组搞定水仙花数,别再手动算啦!
LabVIEW实战:用For循环与数组高效求解水仙花数的5个关键技巧 水仙花数这个经典的编程练习题,在文本编程语言中可能只需十几行代码,但切换到LabVIEW的图形化编程环境时,不少初学者会陷入连线混乱和逻辑纠结。本文将从实际工程视角…...
3步在Mac上免费运行Stable Diffusion的终极指南
3步在Mac上免费运行Stable Diffusion的终极指南 【免费下载链接】MochiDiffusion Run Stable Diffusion on Mac natively 项目地址: https://gitcode.com/gh_mirrors/mo/MochiDiffusion 还在为寻找合适的Mac AI绘画工具而烦恼吗?想要完全离线生成惊艳的AI艺术…...
AutoUnipus:重新定义U校园学习效率的智能解决方案
AutoUnipus:重新定义U校园学习效率的智能解决方案 【免费下载链接】AutoUnipus U校园脚本,支持全自动答题,百分百正确 2024最新版 项目地址: https://gitcode.com/gh_mirrors/au/AutoUnipus 还在为U校园平台上堆积如山的网课任务而焦虑吗?每天花费…...
Web AR技术深度探秘:7个创新案例重构浏览器增强现实体验
Web AR技术深度探秘:7个创新案例重构浏览器增强现实体验 【免费下载链接】AR.js Image tracking, Location Based AR, Marker tracking. All on the Web. 项目地址: https://gitcode.com/gh_mirrors/arj/AR.js 你是一个文章写手,你负责为开源项目…...
从YOLOv5到YOLOv8:停车位检测模型演进与实战性能对比
1. YOLO系列模型的技术演进路径 YOLO(You Only Look Once)系列模型作为目标检测领域的标杆算法,从2015年诞生至今已经经历了多次重大迭代。每次版本更新都带来了显著的性能提升和架构创新,这使得YOLO系列在实时目标检测任务中始终…...
数字图书馆下载工具:高效获取策略与跨平台使用方案
数字图书馆下载工具:高效获取策略与跨平台使用方案 【免费下载链接】internet_archive_downloader A chrome/firefox extension that download books from Internet Archive(archive.org) and HathiTrust Digital Library (hathitrust.org) 项目地址: https://git…...
深度解析DiffSinger:基于扩散模型的AI歌声合成技术革命
深度解析DiffSinger:基于扩散模型的AI歌声合成技术革命 【免费下载链接】DiffSinger 项目地址: https://gitcode.com/gh_mirrors/dif/DiffSinger 在当今AI音乐创作领域,DiffSinger歌声合成技术正引领着一场声音生成的技术革命。这个由OpenVPI维护…...
MangoHud与Vulkan视频编码协议:AV1监控完全指南
MangoHud与Vulkan视频编码协议:AV1监控完全指南 【免费下载链接】MangoHud A Vulkan and OpenGL overlay for monitoring FPS, temperatures, CPU/GPU load and more. Discord: https://discordapp.com/invite/Gj5YmBb 项目地址: https://gitcode.com/gh_mirrors/…...
Obsidian插件本地化全攻略:从英文界面到中文体验的完整实施路径
Obsidian插件本地化全攻略:从英文界面到中文体验的完整实施路径 【免费下载链接】obsidian-i18n 项目地址: https://gitcode.com/gh_mirrors/ob/obsidian-i18n 在全球化协作与知识管理的场景中,Obsidian插件的英文界面常成为用户高效使用的障碍。…...
