Hentikan penutupan dan suntikkan Ketergantungan Injeksi dalam JavaScript

gambar


Pada artikel ini, kita akan melihat bagaimana menulis kode yang bersih dan mudah diuji dalam gaya fungsional menggunakan pola pemrograman Ketergantungan Injeksi. Bonusnya adalah 100% cakupan tes unit.


Terminologi yang akan digunakan dalam artikel


Penulis artikel ini akan mengingat dengan tepat interpretasi dari istilah-istilah berikut, memahami bahwa ini bukan kebenaran tertinggi dan bahwa interpretasi lain dimungkinkan.


  • Ketergantungan injeksi
    Ini adalah pola pemrograman yang mengasumsikan bahwa dependensi eksternal untuk fungsi dan objek pabrik berasal dari luar dalam bentuk argumen untuk fungsi-fungsi ini. Injeksi ketergantungan adalah alternatif untuk menggunakan dependensi dari konteks global.
  • Fungsi bersih
    Ini adalah fungsi, yang hasilnya hanya bergantung pada argumennya. Selain itu, fungsi ini seharusnya tidak memiliki efek samping.
    Saya ingin membuat reservasi segera bahwa fungsi yang kami pertimbangkan tidak memiliki efek samping, tetapi mereka masih dapat memiliki fungsi yang datang kepada kami melalui Injeksi Ketergantungan. Jadi kemurnian fungsi yang kita miliki dengan reservasi besar.
  • Tes unit
    Tes fungsi yang memeriksa bahwa semua garpu di dalam fungsi ini bekerja persis seperti yang diinginkan pembuat kode. Dalam hal ini, alih-alih memanggil fungsi lain, panggilan ke moks digunakan.

Kami mengerti dalam praktiknya


Pertimbangkan sebuah contoh. Pabrik penghitung yang menghitung tick . Penghitung dapat dihentikan menggunakan metode 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 

Kami melihat kode yang dapat dibaca manusia dan dimengerti. Tetapi ada satu tangkapan - tes unit normal tidak dapat ditulis di atasnya. Mari kita lihat apa yang menghalangi?


1) Anda tidak dapat mencapai fungsi di dalam cancel , onInterval dan mengujinya secara terpisah.


2) fungsi onInterval tidak dapat diuji secara terpisah dari fungsi cancel , karena yang pertama memiliki tautan langsung ke yang kedua.


3) dependensi eksternal setInterval , clearInterval .


4) fungsi createCounter tidak dapat diuji secara terpisah dari fungsi lain, lagi karena tautan langsung.


Mari kita selesaikan masalah 1) 2) - kita menghapus fungsi cancel , onInterval dari penutupan dan memutus hubungan langsung di antara mereka melalui objek 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 

Kami memecahkan masalah 3). Kami menggunakan pola Ketergantungan Injeksi pada setInterval , clearInterval dan juga mentransfernya ke objek 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 }) 

Sekarang hampir semuanya baik-baik saja, tetapi masih ada masalah 4). Pada langkah terakhir, kami menerapkan Dependency Injection ke masing-masing fungsi kami dan memutus koneksi yang tersisa di antara mereka melalui objek pool . Pada saat yang sama, kami akan membagi satu file besar menjadi banyak file, sehingga nantinya akan lebih mudah untuk menulis unit test.


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

Kesimpulan


Apa yang kita miliki pada akhirnya? Banyak file, yang masing-masing berisi satu fungsi bersih. Kesederhanaan dan kelengkapan kode sedikit memburuk, tetapi ini lebih dari dikompensasi oleh gambar cakupan 100% dalam unit test.


cakupan


Saya juga ingin mencatat bahwa untuk menulis unit test kita tidak perlu melakukan manipulasi apa pun dengan require dan mendapatkan sistem file Node.js.


Tes unit
 // 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() }) }) 

Hanya dengan membuka semua fungsi sampai akhir, kita mendapatkan kebebasan.

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


All Articles