JS中的依赖注入 — 在测试中未使用过的最佳工具

简介: 让我来为大家介绍在测试中最好的朋友。

13.pngJS中的依赖项注入 — 在测试中未使用过的最佳工具


让我来为大家介绍在测试中最好的朋友。


引言


14.png

代码中的依赖项可以是任何内容,从一个用于执行验证的第三方库,到用于保存所有数据的数据库

依赖项是我们日常开发任务中的一部分,但是当我们写单元测试时,我们总会趋向于忘记,依赖项不是测试中一部分。因此,取而代之的是,往往我们很容易在没有意识到情况下,编写了依赖于这些依赖项的测试。这听起来似乎有点恐怖。 为什么呢? 因为基于以上这种情况,你就不得不检查(在这种情况下产生的)错误结果,同时,还要建立一个相对好的基础设施来获取运行后的测试结果。

上述所说的还不是单元测试的全部内容,在本文中,将向你展示,遇到上述所说的情况时,如何去修复它。

小知识补充:

negative 在这里做名词,表示 (否定的)结果


part1:确切存在的问题是什么呢?


接下来让我们快速地深入到这个问题:为什么我会说你正在不正确地写着单元测试代码呢?

当我们学习单元测试的时候,我们会被告知单元测试是用来验证围绕着代码单元的逻辑测试。 “单元” 这个词的定义因文献而异,但从本质上来讲,单元指的是逻辑中最小的可测试部分。因为已经是最小的可测试部分,那么这也就确保了你不会进行逐行测试,同时也不会去测试一个完整的函数,尤其是当同一时间要去处理多个事务时,更不会去进行逐行测试或者去测试一个完整的函数。

下面,我们尝试着用几个例子来说明 “单元” 这个概念是不固定的。然而,在实际的开发中,几乎没有人展示过以下这两种测试:

  • ①处理将数据写进数据库的测试;
  • ②处理从磁盘中去读取配置文件的测试。

接下来我们来分析,为什么这两种测试会经常被开发者忽略掉呢?

我们在测试中引入的任何与 I/O 有关的活动,无论是有意地还是无意地,这些活动都会迫使你所编写的测试去依赖于当前正在交互的服务。当然,在这里,这些I/O 活动我指的是数据库或者硬盘。但它同样也可以是任何其他东西,比如外部 API

因为我们在测试的时候有可能是在无意中引入,因此,如果是在运行测试时磁盘出现故障,会发生什么情况呢?在这个时候,你的代码将无法读取到代码所对应的文件,这个时候我们可能会想,这算是代码逻辑错误吗?因为通常情况下,代码逻辑错误就是单元测试中失败的原因。但事实上你想错了,这不是代码逻辑错误的问题,而是因为服务有问题而导致的单元测试失败。

这个时候你可能会想,服务有问题跟我的单元测试沾上关系嘞?

事实上,检查外部服务的稳定性不是单元测试的责任,而是集成测试的目的。你必须确保的是,单元测试只关注你的代码问题,因此,你可以通过依赖项注入的方式,来让单元测试做到这一点。

小知识补充:

[1] : meant在这里做非谓语成份,be meant to do sth 译为 按照道理,按照规矩的去做某事

[2] : refer to 谈及,提到

[3] : line-by-line adj. 逐行的

[4] : thing 在日常生活中常译为事情,而在程序中,我们把它译为事务


part2:模式


那么,如何通过依赖项注入的方式,让单元测试只关注你的代码问题呢?

模式也很简单,依赖项注入就是让你能够以某种方式去覆盖一段代码(这段代码也称为客户端)中所具有的依赖项(这些依赖项也称为服务端)。因此,如果你正准备对一个已经写进数据库的函数进行操作,那么你必须以某种方式去覆盖 DB 驱动程序。如果你正准备去处理一个需要调用外部 API 的函数,那么你将会覆盖到一个即将执行 HTTP 请求的库,依次类推。

现在,相信大家已经感受到这个模式的美妙之处了。那依据以上这种模式,我们该如何去实现依赖项注入呢?

如果你是从开始,或者更好的情况下是,你有使用过测试驱动开发 TDD ,那么最简单的方法是先考虑用 TDDTDD 能够为你从模块中导出的每一个方法和函数提供一个简单的覆盖参数。这样,即使你在没有进行测试的时候,就会先有一个默认行为(这个默认行为可以理解为前面所说的覆盖参数)。从而等到你在进行测试时,就有一个覆盖开关可以进行操作。

从另一方面来说,如果你测试的代码没有事先考虑到上面所说的测试驱动开发 (这是最常见的场景),那也问题不大。接下来,你将能够在 JS 中找到实现依赖项注入的不同方法。

小知识补充:

[1] : TDD,即 Test-Driven Development 测试驱动开发


part3:为什么要引入依赖项注入呢?


现在,我们来谈论为什么要引入依赖项注入这个问题。

这是一个很好的问题,我想我在本文的引言中,围绕着这个问题的答案绕了一大圈。现在应该能够得到很明确的答案了👇:

  • 如果你在单元测试中没有使用依赖项注入,那么你正在错误地做着单元测试。

这听起来似乎有点奇怪,让我们来看看为什么会这样子。具体如下:

  • 你不想依赖于存在有潜在地无法控制的外部服务来了解程序的逻辑是否是稳定的。
  • 没有依赖项注入,你就没有办法去完全控制到这些外部服务是如何响应的,从而增加了测试行为的不确定性。
  • 如果这些外部服务出现延迟,那么  它们将直接影响测试的性能。当然,如果只跑10个测试,那可能没啥问题。但是呢,如果你在一个大的系统上工作,即使影响不到1000个测试,也会影响到100个测试。而且,运行测试通常是任何 CI/CD 管道的第一步,如果外部服务出现延迟,这也将会影响到所部署项目的性能

说到这里,相信你已经可以自己提出一些其他的潜在问题了。这里值得注意的要点是,外部服务会降低测试的稳定性,而测试的稳定性应该是一直 100% 的情况才是。我们可以将测试视为幂等元,对于相同受控环境下的每一次执行,结果应该是相同的。这就像有一个使用了全局变量的函数,除非主动去控制该变量,否则是无法真正判断函数的输出结果是否总是相同的。

同样地,在这里,你无法去控制外部服务,因此,就需要去考虑在这种错误结果下会产生的副作用。这也就是为什么要使用依赖项注入的原因。


part4:在JS中如何去执行依赖项注入呢?


多亏了  JS 这门动态语言,使得这变得相当简单。

正如我下面所提及到的,有很多种方法可以去做这件事情。它们将依据具体的情况去进行相应的操作。


1️⃣最佳情况:在编写代码的同时边做测试

在这种情况下,可以先简单地进行一些操作。如下代码所示:

import { query, connect } from "./dbdriver";
function saveData(data, { q = query, con = connect } = {}) {
    /**
    Call 'q' to execute the db query
    Call 'con' to connect to the database
    */
    con();
    const strQuery =
        "insert into mydatabase.mytable (data) value ('" + data + "')";
    if (q(strQuery)) {
        return true;
    } else {
        return {
            error: true,
            msg: "There was a problem saving your data",
        };
    }
}
复制代码

在以上代码中,我们在 saveData 函数中声明了依赖项作为最后的参数,也就是 { q = query, con = connect } = {} 。在这句代码中,值得注意的是,我正在使用解构语法将潜在性存在的覆盖内容给分组到一个单一对象中。同时,在下面的代码中,大家可以发现到,不论它们被定义在何处,我始终引用着 qcon

在正常执行时,我只需要使用第一个参数,也就是 q ,来调用 saveData 函数。同时, q 调用的其他参数将默认为是从数据库驱动程序包中导入的参数,也就是上面代码中的 strQuery

如果我现在在测试上面这个函数,那么我会这么处理。具体代码如下:

describe("My module", () => {
  // 当数据到保存到数据库时,应该返回一个true
    it ("should return true if the data is saved into the database", () => {
    // 此处的 saveData 是全局 require 形式
        const result = await saveData('hi there!', {
            // 查询成功
            q: () => true, 
            // 连接成功
            con: () => true
        });
        result.should.be.true;
    })
}
复制代码

注意看下我是如何重写这两种依赖的。我不再连接数据库,同时,也很明确地表示不会再给数据库发送一个查询

相反,如果是数据没有被成功被保存到数据库的情况下,结果也总是成功的。用这种方法,代码可以这么写。如下所示:

// 当数据库没有被保存到数据库时,应该返回一个error对象
it("should return an error object if the data is not saved into the database", () => {
    const result = await saveData("hi there!", {
        // 查询失败
        q: () => false,
        // 连接成功
        con: () => true,
    });
    result.should.equal({
        error: true,
        msg: "There was a problem saving your data",
    });
});
复制代码

大家看上面这段代码,这次,我将 query 函数总是返回一个 false 的结果,用这种方法,我就可以很安全地测试到函数中的备用逻辑路径

这样,我就不再需要数据库一直处于活动状态并在任何时候运行,至此,测试将毫不延迟地得到运行。

小知识补充:

[1] : declare …… as …… 声明 … 为 …

[2] : group sth 这里的group是动词,表示将什么进行分组

[3] : no matter where … 无论…


2️⃣不理想情况:正在测试已经编写完成的代码并且无法更改它

从另一个方面来说,如果你的任务是将测试添加到一大块已经编写完成的代码中,并且出于某些奇怪的要求,你又不能将它修改成像上面例子所呈现的。那么,这个时候我们就需要找到更具有创造性的方法来解决这个问题。

举个例子,假设你正在写 Node.js 的代码,那么你可以使用类似于 proxyquire 的东西, proxyquire 允许你在不影响代码的情况下替换正在测试的文件中所需的依赖项。比如,假设我们现在有这么一段代码,具体如下:

import { query, connect } from "./dbdriver";
function saveData(data) {
    connect();
    const strQuery =
        "insert into mydatabase.mytable (data) value ('" + data + "')";
    if (query(strQuery)) {
        return true;
    } else {
        return {
            error: true,
            msg: "There was a problem saving your data",
        };
    }
}
复制代码

在上面的这段代码中,想要从外部重写 dbdriver 模块并不容易,但是,如果使用了 proxyquire ,我们就可以在测试的内部去执行这样的操作。具体代码如下:

describe("My module", () => {
  // 如果数据被成功保存到数据库中,应该返回一个true
    it ("should return true if the data is saved into the database", () => {
        const saveData = proxyquire("./saveData.js", {
            './dbdriver': {
                // 查询成功
                q: () => true,
                // 连接成功
                con: () => true
            }
        })
        const result = await saveData('hi there!')
        result.should.be.true;
    })
}
复制代码

现在,大家可以看到,我们将在测试用例中导入 saveData 函数,而不再使用一个全局的 require 。同时,我们还将使用一个自定义返回的对象,来覆盖 dbdriver 在文件中的中 require 调用。从根本意义上来说,我们没有更改到原始代码,但是这个版本的 saveData 将使用我们残留下来的驱动程序,而不是原来的驱动程序。

在上面的例子中,如果你使用的是 browsify ,那么有一个 proxyquire 的版本你可以使用它。点击这里进行查看

当然,还有另外一种情况是,如果你是在 TypeScript 中使用,那么可以访问这两个网址:inversify.io/github.com/typestack/t… 。使用起来肯定没有那么简单,但这两个网址提供了一个极度兼容 TypeScriptAPI

小知识补充:

[1] : override (sth1) with (sth2) 用 sth2 覆盖 sth1

[2] : stub 作名词时表示残余部分,+ed后为stubbed,当形容词使用,表示残留的。

[3] : browsify 是一个 npm 包


结束语


依赖项注入是一个非常好用的工具,它被许多开发人员严重忽视,尤其是在单元测试的时候。不可否认的是,它会在帮助我们编写可扩展且可靠的代码时创造一些奇迹。因此,这非常值得我们去尝试。

JavaScript 中动态的类型和行为是值得我们去做更多尝试的理想选择。所以,多去留意 JS 中的动态美!

JavaScript 中你最喜欢的 DI 库是什么呢?最重要的一点是,你是考虑使用 DI 库进行编码(不理想情况),还是考虑在编写测试代码的时候去构建 DI 呢(最佳情况)?(这句话中的所有 DI 都翻译为依赖项注入

小知识补充:

[1] : dipping your toes into the DI waters 柯林斯词典中将其译为 稍加尝试/谨慎尝试

[2] : 第一部分的 DI water 表示去离子水,第二部分的 DI 是依赖项注入的缩写



相关文章
|
6天前
|
安全 前端开发 测试技术
如何选择合适的自动化安全测试工具
选择合适的自动化安全测试工具需考虑多个因素,包括项目需求、测试目标、系统类型和技术栈,工具的功能特性、市场评价、成本和许可,以及集成性、误报率、社区支持、易用性和安全性。综合评估这些因素,可确保所选工具满足项目需求和团队能力。
|
5天前
|
监控 网络协议 Java
一些适合性能测试脚本编写和维护的工具
一些适合性能测试脚本编写和维护的工具
|
6天前
|
安全 网络协议 关系型数据库
最好用的17个渗透测试工具
渗透测试是安全人员为防止恶意黑客利用系统漏洞而进行的操作。本文介绍了17款业内常用的渗透测试工具,涵盖网络发现、无线评估、Web应用测试、SQL注入等多个领域,包括Nmap、Aircrack-ng、Burp Suite、OWASP ZAP等,既有免费开源工具,也有付费专业软件,适用于不同需求的安全专家。
10 2
|
11天前
|
机器学习/深度学习 自然语言处理 前端开发
前端神经网络入门:Brain.js - 详细介绍和对比不同的实现 - CNN、RNN、DNN、FFNN -无需准备环境打开浏览器即可测试运行-支持WebGPU加速
本文介绍了如何使用 JavaScript 神经网络库 **Brain.js** 实现不同类型的神经网络,包括前馈神经网络(FFNN)、深度神经网络(DNN)和循环神经网络(RNN)。通过简单的示例和代码,帮助前端开发者快速入门并理解神经网络的基本概念。文章还对比了各类神经网络的特点和适用场景,并简要介绍了卷积神经网络(CNN)的替代方案。
|
18天前
|
Web App开发 JavaScript 前端开发
使用 Chrome 浏览器的内存分析工具来检测 JavaScript 中的内存泄漏
【10月更文挑战第25天】利用 Chrome 浏览器的内存分析工具,可以较为准确地检测 JavaScript 中的内存泄漏问题,并帮助我们找出潜在的泄漏点,以便采取相应的解决措施。
127 9
|
20天前
|
监控 前端开发 JavaScript
React 静态网站生成工具 Next.js 入门指南
【10月更文挑战第20天】Next.js 是一个基于 React 的服务器端渲染框架,由 Vercel 开发。本文从基础概念出发,逐步探讨 Next.js 的常见问题、易错点及解决方法,并通过具体代码示例进行说明,帮助开发者快速构建高性能的 Web 应用。
53 10
|
17天前
|
Web App开发 定位技术 iOS开发
Playwright 是一个强大的工具,用于在各种浏览器上测试应用,并模拟真实设备如手机和平板。通过配置 `playwright.devices`,可以轻松模拟不同设备的用户代理、屏幕尺寸、视口等特性。此外,Playwright 还支持模拟地理位置、区域设置、时区、权限(如通知)和配色方案,使测试更加全面和真实。例如,可以在配置文件中设置全局的区域设置和时区,然后在特定测试中进行覆盖。同时,还可以动态更改地理位置和媒体类型,以适应不同的测试需求。
Playwright 是一个强大的工具,用于在各种浏览器上测试应用,并模拟真实设备如手机和平板。通过配置 `playwright.devices`,可以轻松模拟不同设备的用户代理、屏幕尺寸、视口等特性。此外,Playwright 还支持模拟地理位置、区域设置、时区、权限(如通知)和配色方案,使测试更加全面和真实。例如,可以在配置文件中设置全局的区域设置和时区,然后在特定测试中进行覆盖。同时,还可以动态更改地理位置和媒体类型,以适应不同的测试需求。
17 1
|
1月前
|
Java 流计算
Flink-03 Flink Java 3分钟上手 Stream 给 Flink-02 DataStreamSource Socket写一个测试的工具!
Flink-03 Flink Java 3分钟上手 Stream 给 Flink-02 DataStreamSource Socket写一个测试的工具!
37 1
Flink-03 Flink Java 3分钟上手 Stream 给 Flink-02 DataStreamSource Socket写一个测试的工具!
|
1月前
|
jenkins 测试技术 持续交付
提升软件测试效率的实用技巧与工具
【10月更文挑战第12天】 本文将深入探讨如何通过优化测试流程、引入自动化工具和持续集成等策略,来显著提高软件测试的效率。我们将分享一些实用的技巧和工具,帮助测试人员更高效地发现和定位问题,确保软件质量。
45 2
|
1月前
|
存储 JavaScript 前端开发
Node.js 常用工具
10月更文挑战第6天
20 2