
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.
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.

Ich möchte auch darauf hinweisen, dass wir zum Schreiben von Komponententests keine Manipulationen mit require
und das Dateisystem Node.js abrufen müssen.
Nur wenn wir alle Funktionen bis zum Ende öffnen, gewinnen wir Freiheit.