JavaScript中的依赖注入--你在测试中没有使用的最佳工具,看看这个 2021-11-07 默认分类 暂无评论 1235 次阅读 你代码中的依赖关系可以是任何东西,从你用来执行验证的第三方库到你保存所有数据的数据库。 它们是我们日常工作的一部分,但在编写单元测试时,我们往往忘记它们不能成为其中的一部分。所以我们在写测试的时候,反而会在不知不觉中依赖它们。为什么这样不好呢?因为你不得不检查假阴性,你不得不建立相当的基础设施来让你的测试运行。 这不是单元测试的目的,在这篇文章中我将告诉你如何解决这个问题。 问题到底出在哪里? 让我们快速深入了解一下为什么我说你写的单元测试是不正确的。 当我们学习单元测试时,我们被告知它们是用来验证我们代码的一个单元周围的逻辑的测试。单元 "的定义因文献而异,但本质上它指的是逻辑中最小的可测试部分。这确保了你不会逐行测试,但你也不会测试全部的功能,尤其是当它们照顾到多个事情的时候。 然后,我们被一些例子所对待,试图告诉你 "单元 "的概念并不固定。然而,他们几乎没有向你展示过一个处理向你的数据库写数据的测试。或者从磁盘上读取一个配置文件。 你在测试中引入的任何`I/O`操作,无论你是有意识的还是无意识的,都是在迫使你的测试依赖于你正在交互的服务。是的,我指的是数据库或硬盘,但它可以是任何东西,例如外部`API`。 如果你正在运行你的测试,而磁盘发生故障,会发生什么?你的代码将无法读取该文件,但你的逻辑有问题吗?因为这就是你的单元测试失败所暗示的。但事实并非如此,服务是有问题的。 检查外部服务的稳定性不是你的单元测试的责任,那是集成测试的作用。你必须确保你的单元测试只关注你的代码,你可以通过使用依赖性注入来做到这一点。 模式 模式很简单,依赖注入是关于你能够以某种方式覆盖一段代码(也称为服务)的依赖关系(又称客户)。因此,如果你正在处理一个写到数据库的函数,你必须以某种方式覆盖数据库驱动。如果你处理的是一个调用外部API的函数,你要覆盖做HTTP请求的库,以此类推。 现在,这种模式的魅力在于你如何实现它。如果你从头开始,甚至更好,如果你使用`TDD`,最简单的方法是提前考虑这个问题,在你从模块中导出的每个公共方法和函数上提供一个简单的覆盖参数。这样你就可以在不测试时有一个默认行为,而在写测试时有一个 "覆盖开关"。 另一方面,如果你在测试代码时没有事先考虑到这一点(这是最常见的情况),你就可以在`JavaScript`中找到不同的方法来实现这一点。 但为什么要这样做呢? 这是一个很好的问题,我想我在这篇文章的介绍中已经对这个问题的答案进行了讨论,但是让我们先明确一下。 > 如果你没有在你的单元测试中使用依赖注入,你就做错了。 说完这一点,让我们看看这是为什么。 - 你不希望依赖你可能无法控制的外部服务,来了解你的逻辑是否稳定。 - 没有它,你就不能完全控制这些外部服务的反应,从而给你的测试行为增加不确定性。 - 如果这些服务出现延迟,它们将直接影响你的测试性能。如果你有10个测试,这不是一个问题,但如果你在一个大系统上工作,这可能会影响100个甚至1000个测试。而运行测试通常是任何`CI/CD`管道的第一步,所以你也会影响你的部署性能。 我相信你自己也能想出一些其他的潜在问题。这里的重点是,这一切都在减少你的测试的稳定性,而你的测试应该一直是100%。把你的测试想成是`idempotents`,在相同的控制环境下每次执行,结果都应该是一样的。这就像有一个使用全局变量的函数,除非你控制这个变量,否则你无法真正判断它的输出是否总是相同的。 在这里,你不能控制外部服务,因此你允许以假阴性的形式出现副作用。这就是为什么你要使用依赖性注入。 如何在`JavaScript`中进行依赖性注入? 其实很简单,这要归功于这种语言的动态特性。 正如我已经提到的,有很多方法可以做到这一点,而且它们都取决于你的情况。 最好的情况是:你在构建代码的同时测试它 在这些情况下,你可以简单就像这样: ```javascript 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`和`con`,无论它们在哪里被定义。 在正常的执行过程中,我可以简单地调用`saveData`函数,只带第一个参数,其他参数将默认为从数据库驱动包中导入的参数。 然而,如果我在测试这个函数,我可以做这样的事情。 ```javascript describe("My module", () => { it ("should return true if the data is saved into the database", () => { const result = await saveData('hi there!', {q: () => true, con: () => true}) result.should.be.true; }) } ``` 注意我是如何重写这两个依赖关系的。我没有再连接数据库,也绝对没有向它发送查询。相反,我强迫结果总是成功的。这样的话,我也可以这样做。 ```javascript 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" }) }) ``` 我决定这次的查询函数将总是返回`false`,这样我就可以安全地测试我的函数的备用逻辑路径。 这样我就不需要数据库在任何时候都处于激活和运行状态,我的测试就会毫无延迟地运行。 不理想:你在测试已经写好的代码,你不能改变它 另一方面,如果你的任务是给一段已经写好的代码添加测试,而由于一些奇怪的要求,你不能把它修改成上面的例子那样,那么你就必须找到更有创意的方法。 例如,如果你在`Node.js`中工作,你可以使用像`proxyquire`这样的东西,它允许你在不影响其代码的情况下替换你正在测试的文件中所需要的依赖性。例如,设想我们的代码是这样的。 ```javascript 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`,你可以在你的测试中这样做。 ```javascript describe("My module", () => { 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`,还有其他的替代品,比如 `https://inversify.io/` 和 [typestack/typedi](http://www.guobacai.com/index.php/go/url-186-typedi/ "typestack/typedi") ,它们肯定没有那么直接,但它们确实提供了一个非常兼容`TypeScript`的`API`。 依赖注入是一个非常有用的工具,一个被许多开发者严重忽视的工具,特别是在涉及到单元测试时。然而,它可以在帮助你编写可扩展和可靠的代码方面创造奇迹,所以请尝试一下。`JavaScript`的动态类型和行为是你开始涉足`DI`领域的理想选择,所以请看看它吧 你最喜欢的`JavaScript`的`DI库`是什么?最重要的是,你在编写代码时是考虑到了`DI`,还是在编写代码的测试时考虑到了`DI`? 用独立的组件构建,以获得速度和规模 与其构建单一的应用程序,不如先构建独立的组件,然后将它们组成功能和应用程序。它使开发更快,并帮助团队建立更一致和可扩展的应用程序。 `Bit`为构建独立组件和合成应用提供了很好的开发者体验。许多团队开始通过独立组件来构建他们的设计系统或微前端。 代码地址:[点击查看](http://www.guobacai.com/index.php/go/url-186/ "点击查看") 标签: javascript, 单元测试, 数据库, 前端自动化测试, TypeScript
评论已关闭