中断闭包并在JavaScript中注入依赖项注入

图片


在本文中,我们将研究如何使用Dependency Injection编程模式以功能风格编写干净,易于测试的代码。 奖励是100%单元测试覆盖率。


本文中将使用的术语


本文的作者将牢记以下术语的这种解释,并理解这不是最终的真理,其他解释也是可能的。


  • 依赖注入
    这是一种编程模式,它假定函数和对象工厂的外部依赖关系是从外部以这些函数的参数形式出现的。 依赖注入是使用全局上下文中的依赖的替代方法。
  • 净功能
    这是一个函数,其结果仅取决于其参数。 此外,该功能不应有副作用。
    我想立即保留一下我们正在考虑的功能没有副作用,但它们仍然可以具有通过依赖注入获得的功能。 因此,我们保留的功能纯洁。
  • 单元测试
    一个功能测试,检查该功能内的所有派生类是否与预期代码的作者完全相同。 在这种情况下,不是调用任何其他函数,而是使用对moks的调用。

我们在实践中了解


考虑一个例子。 有tick的柜台工厂。 可以使用cancel方法停止计数器。


 const createCounter = ({ ticks, onTick }) => { const state = { currentTick: 1, timer: null, canceled: false } const cancel = () => { if (state.canceled) { throw new Error('"Counter" already canceled') } clearInterval(state.timer) } const onInterval = () => { onTick(state.currentTick++) if (state.currentTick > ticks) { cancel() } } state.timer = setInterval(onInterval, 200) const instance = { cancel } return instance } export default createCounter 

我们看到了人类可读,可理解的代码。 但是有一个问题-不能在上面写普通的单元测试。 让我们看看有什么问题吗?


1)您无法访问cancelonInterval内部的功能并分别对其进行测试。


2) onInterval函数不能与cancel函数分开测试,因为 第一个与第二个有直接链接。


3)使用外部依赖setIntervalclearInterval


4)同样由于直接链接,无法将createCounter函数与其他函数分开进行测试。


让我们解决问题1)2)-从闭包中删除cancelonInterval ,并通过pool对象断开它们之间的直接链接。


 export const cancel = pool => { if (pool.state.canceled) { throw new Error('"Counter" already canceled') } clearInterval(pool.state.timer) } export const onInterval = pool => { pool.config.onTick(pool.state.currentTick++) if (pool.state.currentTick > pool.config.ticks) { pool.cancel() } } const createCounter = config => { const pool = { config, state: { currentTick: 1, timer: null, canceled: false } } pool.cancel = cancel.bind(null, pool) pool.onInterval = onInterval.bind(null, pool) pool.state.timer = setInterval(pool.onInterval, 200) const instance = { cancel: pool.cancel } return instance } export default createCounter 

我们解决问题3)。 我们在setIntervalclearInterval上使用了依赖注入模式,并将它们转移到pool对象中。


 export const cancel = pool => { const { clearInterval } = pool if (pool.state.canceled) { throw new Error('"Counter" already canceled') } clearInterval(pool.state.timer) } export const onInterval = pool => { pool.config.onTick(pool.state.currentTick++) if (pool.state.currentTick > pool.config.ticks) { pool.cancel() } } const createCounter = (dependencies, config) => { const pool = { ...dependencies, config, state: { currentTick: 1, timer: null, canceled: false } } pool.cancel = cancel.bind(null, pool) pool.onInterval = onInterval.bind(null, pool) const { setInterval } = pool pool.state.timer = setInterval(pool.onInterval, 200) const instance = { cancel: pool.cancel } return instance } export default createCounter.bind(null, { setInterval, clearInterval }) 

现在几乎一切都很好,但是仍然存在问题4)。 在最后一步,我们将依赖注入应用于每个函数,并通过pool对象断开它们之间的其余连接。 同时,我们将一个大文件分成许多文件,以便以后编写单元测试变得更加容易。


 // index.js import { createCounter } from './create-counter' import { cancel } from './cancel' import { onInterval } from './on-interval' export default createCounter.bind(null, { cancel, onInterval, setInterval, clearInterval }) 

 // create-counter.js export const createCounter = (dependencies, config) => { const pool = { ...dependencies, config, state: { currentTick: 1, timer: null, canceled: false } } pool.cancel = dependencies.cancel.bind(null, pool) pool.onInterval = dependencies.onInterval.bind(null, pool) const { setInterval } = pool pool.state.timer = setInterval(pool.onInterval, 200) const instance = { cancel: pool.cancel } return instance } 

 // on-interval.js export const onInterval = pool => { pool.config.onTick(pool.state.currentTick++) if (pool.state.currentTick > pool.config.ticks) { pool.cancel() } } 

 // cancel.js export const cancel = pool => { const { clearInterval } = pool if (pool.state.canceled) { throw new Error('"Counter" already canceled') } clearInterval(pool.state.timer) } 

结论


到底我们有什么? 一堆文件,每个文件包含一个清理功能。 代码的简单性和可理解性有所降低,但这远远超出了单元测试中100%覆盖率的补偿。


覆盖范围


我还想指出,编写单元测试时,我们不需要对require进行任何操作即可获取文件系统Node.js。


单元测试
 // cancel.test.js import { cancel } from '../src/cancel' describe('method "cancel"', () => { test('should stop the counter', () => { const state = { canceled: false, timer: 42 } const clearInterval = jest.fn() const pool = { state, clearInterval } cancel(pool) expect(clearInterval).toHaveBeenCalledWith(pool.state.timer) }) test('should throw error: "Counter" already canceled', () => { const state = { canceled: true, timer: 42 } const clearInterval = jest.fn() const pool = { state, clearInterval } expect(() => cancel(pool)).toThrow('"Counter" already canceled') expect(clearInterval).not.toHaveBeenCalled() }) }) 

 // create-counter.test.js import { createCounter } from '../src/create-counter' describe('method "createCounter"', () => { test('should create a counter', () => { const boundCancel = jest.fn() const boundOnInterval = jest.fn() const timer = 42 const cancel = { bind: jest.fn().mockReturnValue(boundCancel) } const onInterval = { bind: jest.fn().mockReturnValue(boundOnInterval) } const setInterval = jest.fn().mockReturnValue(timer) const dependencies = { cancel, onInterval, setInterval } const config = { ticks: 42 } const counter = createCounter(dependencies, config) expect(cancel.bind).toHaveBeenCalled() expect(onInterval.bind).toHaveBeenCalled() expect(setInterval).toHaveBeenCalledWith(boundOnInterval, 200) expect(counter).toHaveProperty('cancel') }) }) 

 // on-interval.test.js import { onInterval } from '../src/on-interval' describe('method "onInterval"', () => { test('should call "onTick"', () => { const onTick = jest.fn() const cancel = jest.fn() const state = { currentTick: 1 } const config = { ticks: 5, onTick } const pool = { onTick, cancel, state, config } onInterval(pool) expect(onTick).toHaveBeenCalledWith(1) expect(pool.state.currentTick).toEqual(2) expect(cancel).not.toHaveBeenCalled() }) test('should call "onTick" and "cancel"', () => { const onTick = jest.fn() const cancel = jest.fn() const state = { currentTick: 5 } const config = { ticks: 5, onTick } const pool = { onTick, cancel, state, config } onInterval(pool) expect(onTick).toHaveBeenCalledWith(5) expect(pool.state.currentTick).toEqual(6) expect(cancel).toHaveBeenCalledWith() }) }) 

只有将所有功能放到最后,我们才能获得自由。

Source: https://habr.com/ru/post/zh-CN440552/


All Articles