Romper cierres e inyectar inyecci贸n de dependencia en JavaScript

imagen


En este art铆culo, veremos c贸mo escribir c贸digo limpio y f谩cilmente comprobable en un estilo funcional utilizando el patr贸n de programaci贸n de Inyecci贸n de dependencias. La bonificaci贸n es el 100% de cobertura de prueba unitaria.


La terminolog铆a que se utilizar谩 en el art铆culo.


El autor del art铆culo tendr谩 en cuenta precisamente esa interpretaci贸n de los siguientes t茅rminos, entendiendo que esta no es la verdad 煤ltima y que otras interpretaciones son posibles.


  • Inyecci贸n de dependencia
    Este es un patr贸n de programaci贸n que asume que las dependencias externas para funciones y f谩bricas de objetos provienen del exterior en forma de argumentos para estas funciones. La inyecci贸n de dependencias es una alternativa al uso de dependencias de un contexto global.
  • Funci贸n de red
    Esta es una funci贸n, cuyo resultado depende solo de sus argumentos. Adem谩s, la funci贸n no deber铆a tener efectos secundarios.
    Quiero hacer una reserva de inmediato que las funciones que estamos considerando no tienen efectos secundarios, pero a煤n pueden tener las funciones que nos llegaron a trav茅s de la inyecci贸n de dependencia. Entonces, la pureza de las funciones que tenemos con una gran reserva.
  • Prueba unitaria
    Una prueba de funci贸n que verifica que todas las bifurcaciones dentro de esta funci贸n funcionen exactamente como el autor del c贸digo pretend铆a. En este caso, en lugar de llamar a otras funciones, se usa una llamada a moks.

Entendemos en la pr谩ctica


Considera un ejemplo. Una f谩brica de contadores que cuentan tick . El contador se puede detener utilizando el m茅todo de 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 

Vemos un c贸digo comprensible y legible para humanos. Pero hay un inconveniente: las pruebas unitarias normales no se pueden escribir en 茅l. 驴Veamos qu茅 hay en el camino?


1) no puede alcanzar las funciones dentro de la cancel , en el onInterval y probarlas por separado.


2) la funci贸n onInterval no se puede probar por separado de la funci贸n cancel , porque el primero tiene un enlace directo al segundo.


3) clearInterval las dependencias externas setInterval , clearInterval .


4) la funci贸n createCounter no se puede probar por separado de otras funciones, nuevamente debido a los enlaces directos.


Solucionemos los problemas 1) 2): eliminamos las onInterval cancel , onInterval del cierre y rompemos los enlaces directos entre ellas a trav茅s del objeto 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 

Resolvemos el problema 3). Usamos el patr贸n de inyecci贸n de dependencia en setInterval , clearInterval y tambi茅n los transferimos al objeto de 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 }) 

Ahora casi todo est谩 bien, pero todav铆a hay un problema 4). En el 煤ltimo paso, aplicamos la Inyecci贸n de dependencias a cada una de nuestras funciones y rompemos las conexiones restantes entre ellas a trav茅s del objeto pool . Al mismo tiempo, dividiremos un archivo grande en muchos archivos, para que luego sea m谩s f谩cil escribir pruebas unitarias.


 // 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) } 

Conclusi贸n


驴Qu茅 tenemos al final? Un mont贸n de archivos, cada uno de los cuales contiene una funci贸n limpia. La simplicidad y la comprensibilidad del c贸digo se han deteriorado un poco, pero esto se ve m谩s que compensado por la imagen del 100% de cobertura en las pruebas unitarias.


cobertura


Tambi茅n quiero se帽alar que para escribir pruebas unitarias no necesitamos hacer ninguna manipulaci贸n con require y obtener el sistema de archivos Node.js.


Pruebas unitarias
 // 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() }) }) 

Solo al abrir todas las funciones hasta el final, ganamos libertad.

Source: https://habr.com/ru/post/440552/


All Articles