Schließen Sie Schließungen und injizieren Sie Dependency Injection in JavaScript

Bild


In diesem Artikel erfahren Sie, wie Sie sauberen, leicht testbaren Code in einem funktionalen Stil mithilfe des Programmiermusters "Dependency Injection" schreiben. Der Bonus ist 100% Unit Test Coverage.


Die Terminologie, die im Artikel verwendet wird


Der Autor des Artikels wird genau eine solche Interpretation der folgenden Begriffe berücksichtigen, wobei er versteht, dass dies nicht die endgültige Wahrheit ist und dass andere Interpretationen möglich sind.


  • Abhängigkeitsinjektion
    Dies ist ein Programmiermuster, bei dem davon ausgegangen wird, dass externe Abhängigkeiten für Funktionen und Objektfabriken in Form von Argumenten für diese Funktionen von außen kommen. Die Abhängigkeitsinjektion ist eine Alternative zur Verwendung von Abhängigkeiten aus einem globalen Kontext.
  • Netzfunktion
    Dies ist eine Funktion, deren Ergebnis nur von ihren Argumenten abhängt. Außerdem sollte die Funktion keine Nebenwirkungen haben.
    Ich möchte sofort reservieren, dass die Funktionen, die wir in Betracht ziehen, keine Nebenwirkungen haben, aber dennoch die Funktionen haben können, die durch Dependency Injection zu uns gekommen sind. Also die Reinheit der Funktionen haben wir mit großem Vorbehalt.
  • Unit Test
    Ein Funktionstest, der überprüft, ob alle Gabeln in dieser Funktion genau wie der Autor des beabsichtigten Codes funktionieren. In diesem Fall wird anstelle anderer Funktionen ein Aufruf von moks verwendet.

Wir verstehen in der Praxis


Betrachten Sie ein Beispiel. Eine Fabrik von Zählern, die tick zählen. Der Zähler kann mit der cancel gestoppt werden.


 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 

Wir sehen für Menschen lesbaren, verständlichen Code. Aber es gibt einen Haken: Normale Unit-Tests können nicht darauf geschrieben werden. Mal sehen, was im Weg ist?


1) Sie können die Funktionen innerhalb des onInterval , beim onInterval nicht erreichen und separat testen.


2) Die onInterval Funktion onInterval nicht getrennt von der cancel Funktion getestet werden, weil Der erste hat einen direkten Link zum zweiten.


3) Die externen Abhängigkeiten setInterval , clearInterval .


4) Die Funktion createCounter aufgrund direkter Links nicht getrennt von anderen Funktionen getestet werden.


Lösen wir die Probleme 1) 2) - Wir entfernen die onInterval cancel , onInterval aus dem Abschluss und onInterval die direkten Verbindungen zwischen ihnen durch das pool Objekt.


 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 

Wir lösen das Problem 3). Wir verwenden das Abhängigkeitsinjektionsmuster für setInterval , clearInterval und übertragen sie auch auf das clearInterval .


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

Jetzt ist fast alles in Ordnung, aber es gibt immer noch ein Problem 4). Im letzten Schritt wenden wir Dependency Injection auf jede unserer Funktionen an und unterbrechen die verbleibenden Verbindungen zwischen ihnen durch das pool . Gleichzeitig werden wir eine große Datei in mehrere Dateien aufteilen, damit es später einfacher ist, Komponententests zu schreiben.


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

Fazit


Was haben wir am Ende? Eine Reihe von Dateien, von denen jede eine Bereinigungsfunktion enthält. Die Einfachheit und Verständlichkeit des Codes hat sich etwas verschlechtert, dies wird jedoch durch das Bild einer 100% igen Abdeckung in Unit-Tests mehr als kompensiert.


Abdeckung


Ich möchte auch darauf hinweisen, dass wir zum Schreiben von Komponententests keine Manipulationen mit require und das Dateisystem Node.js abrufen müssen.


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

Nur wenn wir alle Funktionen bis zum Ende öffnen, gewinnen wir Freiheit.

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


All Articles