Quebrar fechamentos e injetar injeção de dependência em JavaScript

imagem


Neste artigo, veremos como escrever código limpo e facilmente testável em um estilo funcional usando o padrão de programação Injeção de Dependência. O bônus é 100% de cobertura de teste de unidade.


A terminologia que será usada no artigo


O autor do artigo terá em mente exatamente essa interpretação dos termos a seguir, entendendo que essa não é a verdade última e que outras interpretações são possíveis.


  • Injeção de dependência
    Esse é um padrão de programação que assume que dependências externas para funções e fábricas de objetos vêm de fora na forma de argumentos para essas funções. A injeção de dependência é uma alternativa ao uso de dependências de um contexto global.
  • Função líquida
    Essa é uma função, cujo resultado depende apenas de seus argumentos. Além disso, a função não deve ter efeitos colaterais.
    Quero fazer uma reserva imediatamente de que as funções que estamos considerando não têm efeitos colaterais, mas ainda podem ter as funções que chegaram até nós através da Injeção de Dependência. Portanto, a pureza das funções que temos com uma grande reserva.
  • Teste de unidade
    Um teste de função que verifica se todos os garfos dessa função funcionam exatamente como o autor do código pretendido. Nesse caso, em vez de chamar outras funções, é usada uma chamada para moks.

Entendemos na prática


Considere um exemplo. Uma fábrica de balcões que contam tick . O contador pode ser parado usando o método 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 código legível por humanos e compreensível. Mas há um problema: testes de unidade normais não podem ser escritos nele. Vamos ver o que está no caminho?


1) você não pode acessar as funções dentro do cancel , onInterval e testá-las separadamente.


2) a função onInterval não onInterval ser testada separadamente da função cancel , porque o primeiro tem um link direto para o segundo.


3) as dependências externas setInterval , clearInterval .


4) a função createCounter não createCounter ser testada separadamente de outras funções, novamente devido a links diretos.


Vamos resolver os problemas 1) 2) - removemos as onInterval cancel , onInterval do fechamento e quebramos os links diretos entre elas através do 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 

Nós resolvemos o problema 3). Usamos o padrão Injeção de Dependência em setInterval , clearInterval e também os transferimos para o 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 }) 

Agora quase tudo está bem, mas ainda há um problema 4). Na última etapa, aplicamos a Injeção de Dependência em cada uma de nossas funções e interrompemos as conexões restantes entre elas através do objeto pool . Ao mesmo tempo, dividiremos um arquivo grande em muitos, para que mais tarde seja mais fácil escrever testes de unidade.


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

Conclusão


O que temos no final? Um monte de arquivos, cada um dos quais contém uma função limpa. A simplicidade e a compreensibilidade do código se deterioraram um pouco, mas isso é mais do que compensado pela imagem de 100% de cobertura em testes de unidade.


cobertura


Também quero observar que, para escrever testes de unidade, não precisamos manipular o require e obter o sistema de arquivos Node.js.


Testes unitários
 // 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() }) }) 

Somente ao abrir todas as funções até o fim, obtemos liberdade.

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


All Articles