Interrompre les fermetures et injecter l'injection de dépendance en JavaScript

image


Dans cet article, nous verrons comment écrire du code propre et facilement testable dans un style fonctionnel à l'aide du modèle de programmation par injection de dépendances. Le bonus est une couverture de test unitaire à 100%.


La terminologie qui sera utilisée dans l'article


L'auteur de l'article gardera à l'esprit précisément une telle interprétation des termes suivants, sachant que ce n'est pas la vérité ultime et que d'autres interprétations sont possibles.


  • Injection de dépendance
    Il s'agit d'un modèle de programmation qui suppose que les dépendances externes pour les fonctions et les fabriques d'objets proviennent de l'extérieur sous forme d'arguments à ces fonctions. L'injection de dépendances est une alternative à l'utilisation de dépendances à partir d'un contexte global.
  • Fonction nette
    Il s'agit d'une fonction dont le résultat ne dépend que de ses arguments. De plus, la fonction ne devrait pas avoir d'effets secondaires.
    Je veux faire une réserve tout de suite que les fonctions que nous envisageons n'ont pas d'effets secondaires, mais elles peuvent toujours avoir les fonctions qui nous sont venues grâce à l'injection de dépendance. Donc la pureté des fonctions que nous avons avec une grande réserve.
  • Test unitaire
    Un test de fonction qui vérifie que toutes les fourches à l'intérieur de cette fonction fonctionnent exactement comme l'auteur du code prévu. Dans ce cas, au lieu d'appeler d'autres fonctions, un appel à moks est utilisé.

Nous comprenons dans la pratique


Prenons un exemple. Une fabrique de compteurs qui comptent les tick . Le compteur peut être arrêté à l'aide de la méthode d' 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 

Nous voyons du code lisible par l'homme et compréhensible. Mais il y a un hic - les tests unitaires normaux ne peuvent pas y être écrits. Voyons ce qui est sur le chemin?


1) vous ne pouvez pas accéder aux fonctions à l'intérieur de l' cancel , de la onInterval et les tester séparément.


2) la fonction onInterval ne onInterval pas être testée séparément de la fonction d' cancel , car le premier a un lien direct avec le second.


3) les dépendances externes setInterval , clearInterval .


4) la fonction createCounter ne createCounter pas être testée séparément des autres fonctions, encore une fois en raison de liens directs.


onInterval les problèmes 1) 2) - nous onInterval d' cancel , onInterval de la fermeture et rompons les liens directs entre eux via l'objet 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 

Nous résolvons le problème 3). Nous utilisons le modèle d'Injection de dépendances sur setInterval , clearInterval et les transférons également à l'objet 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 }) 

Maintenant, tout va bien, mais il y a toujours un problème 4). Dans la dernière étape, nous appliquons l'injection de dépendance à chacune de nos fonctions et rompons les connexions restantes entre elles via l'objet pool . En même temps, nous divisons un grand fichier en plusieurs fichiers, de sorte que plus tard, il sera plus facile d'écrire des tests unitaires.


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

Conclusion


Qu'avons-nous finalement? Un tas de fichiers, chacun contenant une fonction propre. La simplicité et l'intelligibilité du code se sont un peu détériorées, mais cela est plus que compensé par l'image d'une couverture à 100% dans les tests unitaires.


couverture


Je veux également noter que pour écrire des tests unitaires, nous n'avons pas besoin de faire de manipulations avec require et d'obtenir le système de fichiers Node.js.


Tests unitaires
 // 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() }) }) 

Ce n'est qu'en ouvrant toutes les fonctions jusqu'au bout que nous gagnons en liberté.

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


All Articles