كسر الإغلاق وحقن حقن التبعية في جافا سكريبت

الصورة


في هذه المقالة ، سننظر في كيفية كتابة تعليمات برمجية نظيفة وقابلة للاختبار بسهولة بأسلوب وظيفي باستخدام نمط برمجة Dependency Injection. المكافأة هي تغطية اختبار 100 ٪ وحدة.


المصطلحات التي سيتم استخدامها في المقال


سيضع مؤلف المقال في الاعتبار بدقة مثل هذا التفسير للمصطلحات التالية ، مع العلم أن هذه ليست الحقيقة المطلقة وأن التفسيرات الأخرى ممكنة.


  • حقن التبعية
    هذا هو نمط البرمجة الذي يفترض أن التبعيات الخارجية للوظائف ومصانع الكائنات تأتي من الخارج في شكل وسيطات لهذه الوظائف. حقن التبعية هو بديل لاستخدام التبعيات من سياق عالمي.
  • وظيفة صافي
    هذه دالة تعتمد نتائجه فقط على وسيطاتها. أيضا ، لا ينبغي أن يكون لهذه الوظيفة آثار جانبية.
    أريد أن أبدي تحفظًا على الفور بأن الوظائف التي ندرسها ليس لها آثار جانبية ، لكن لا يزال بإمكانها الحصول على الوظائف التي أتت إلينا من خلال Dependency Injection. لذلك نقاء الوظائف لدينا مع تحفظ كبير.
  • اختبار الوحدة
    اختبار الوظيفة الذي يتحقق من أن جميع الشوكات داخل هذه الوظيفة تعمل تمامًا مثل مؤلف الرمز المقصود. في هذه الحالة ، بدلاً من استدعاء أي وظائف أخرى ، يتم استخدام مكالمة إلى moks.

ونحن نفهم في الممارسة العملية


النظر في مثال. مصنع من العدادات التي تحسب tick . يمكن إيقاف العداد باستخدام طريقة 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 

نرى رمزًا مقروءًا إنسانيًا وقابلًا للفهم. لكن هناك اختبارًا واحدًا - لا يمكن كتابة اختبارات الوحدة العادية عليه. دعونا نرى ما هو في الطريق؟


1) لا يمكنك الوصول إلى الوظائف داخل cancel ، onInterval واختبارها بشكل منفصل.


2) لا يمكن اختبار وظيفة onInterval بشكل منفصل عن وظيفة cancel ، لأن الأول له صلة مباشرة بالثانية.


3) التبعيات الخارجية setInterval ، clearInterval .


4) لا يمكن اختبار وظيفة createCounter بشكل منفصل عن الوظائف الأخرى ، مرة أخرى بسبب الروابط المباشرة.


دعونا نحل المشاكل 1) 2) - نزيل onInterval cancel onInterval من الإغلاق onInterval الروابط المباشرة بينهما من خلال كائن 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 

نحن نحل المشكلة 3). نحن نستخدم نمط حقن التبعية على setInterval ، clearInterval أيضًا إلى كائن 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 }) 

الآن كل شيء على ما يرام تقريبا ، ولكن لا تزال هناك مشكلة 4). في الخطوة الأخيرة ، نطبق Dependency Injection على كل من وظائفنا ونقطع الاتصالات المتبقية بينهما من خلال كائن pool . في الوقت نفسه ، نقسم ملفًا كبيرًا واحدًا إلى العديد من الملفات ، وبالتالي سيكون من السهل في وقت لاحق كتابة اختبارات الوحدة.


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

الخاتمة


ماذا لدينا في النهاية؟ مجموعة من الملفات ، يحتوي كل منها على وظيفة واحدة نظيفة. تدهورت البساطة وشمولية الكود قليلاً ، ولكن هذا يعوض أكثر من صورة تغطية 100٪ في اختبارات الوحدة.


التغطية


أريد أيضًا أن أشير إلى أنه لكتابة اختبارات الوحدة ، لا نحتاج إلى القيام بأي معالجات require والحصول على نظام الملفات Node.js.


اختبارات الوحدة
 // 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() }) }) 

فقط عن طريق فتح جميع الوظائف حتى النهاية ، نحن نكتسب الحرية.

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


All Articles