सभी को नमस्कार!
इस पोस्ट में, मैं एक प्रतिक्रिया परियोजना में Redux Thunk का उपयोग करके कोड को व्यवस्थित करने और परीक्षण करने के लिए अपने दृष्टिकोण को साझा करना चाहूंगा।
इसके लिए रास्ता लंबा और कांटेदार था, इसलिए मैं विचार और प्रेरणा की ट्रेन को प्रदर्शित करने का प्रयास करूंगा, जो अंतिम निर्णय का कारण बना।
आवेदन और समस्या बयान का विवरण
पहला, थोड़ा संदर्भ।
नीचे दिया गया आंकड़ा हमारी परियोजना में एक विशिष्ट पृष्ठ का लेआउट दिखाता है।

क्रम में:
- तालिका (नंबर 1) में डेटा होता है जो बहुत अलग हो सकता है (सादा पाठ, लिंक, चित्र आदि)।
- सॉर्टिंग पैनल (नंबर 2) स्तंभ द्वारा तालिका में डेटा सॉर्टिंग सेटिंग्स सेट करता है।
- फ़िल्टरिंग पैनल (नंबर 3) तालिका के कॉलम के अनुसार विभिन्न फ़िल्टर सेट करता है।
- कॉलम पैनल (नंबर 4) आपको टेबल कॉलम (प्रदर्शन / छिपाने) के प्रदर्शन को सेट करने की अनुमति देता है।
- टेम्पलेट्स का पैनल (नंबर 5) आपको पहले से तैयार किए गए सेटिंग टेम्प्लेट का चयन करने की अनुमति देता है। टेम्पलेट्स में पैनल नंबर 2, नंबर 3, नंबर 4 के साथ-साथ कुछ अन्य डेटा भी शामिल हैं, उदाहरण के लिए, कॉलम की स्थिति, उनका आकार, आदि।
पैनल संबंधित बटन पर क्लिक करके खोले जाते हैं।
किसी तालिका में कौन से स्तंभ सामान्य रूप से हो सकते हैं, उनमें कौन सा डेटा हो सकता है, उन्हें कैसे प्रदर्शित किया जाना चाहिए, कौन से फ़िल्टर फ़िल्टर हो सकते हैं और अन्य जानकारी तालिका के मेटा-डेटा में समाहित है, जो पेज लोडिंग की शुरुआत में डेटा से अलग से अनुरोध किया जाता है।
यह पता चला है कि तालिका की वर्तमान स्थिति और इसमें डेटा तीन कारकों पर निर्भर करता है:
- तालिका के मेटा डेटा से डेटा।
- वर्तमान में चयनित टेम्पलेट के लिए सेटिंग्स।
- उपयोगकर्ता सेटिंग्स (चयनित टेम्पलेट के बारे में कोई भी परिवर्तन एक तरह के "ड्राफ्ट" में सहेजे जाते हैं, जिसे एक नए टेम्पलेट में बदल दिया जा सकता है, या तो नई सेटिंग्स के साथ वर्तमान को अपडेट करें, या उन्हें हटा दें और टेम्पलेट को अपनी मूल स्थिति में लौटा दें)।
जैसा कि ऊपर उल्लेख किया गया है, ऐसा पृष्ठ विशिष्ट है। ऐसे प्रत्येक पृष्ठ (या अधिक सटीक रूप से, इसमें तालिका के लिए) के लिए, Redux रिपॉजिटरी में अपने डेटा और मापदंडों के साथ संचालन की सुविधा के लिए एक अलग इकाई बनाई जाती है।
ठग और एक्शन क्रिएटर्स के सजातीय सेट और एक विशिष्ट इकाई पर डेटा अपडेट करने में सक्षम होने के लिए, निम्नलिखित दृष्टिकोण का उपयोग किया जाता है (एक प्रकार का कारखाना):
export const actionsCreator = (prefix, getCurrentStore, entityModel) => { function fetchTotalCounterStart() { return { type: `${prefix}FETCH_TOTAL_COUNTER_START` }; } function fetchTotalCounterSuccess(payload) { return { type: `${prefix}FETCH_TOTAL_COUNTER_SUCCESS`, payload }; } function fetchTotalCounterError(error) { return { type: `${prefix}FETCH_TOTAL_COUNTER_ERROR`, error }; } function applyFilterSuccess(payload) { return { type: `${prefix}APPLY_FILTER_SUCCESS`, payload }; } function applyFilterError(error) { return { type: `${prefix}APPLY_FILTER_ERROR`, error }; } function fetchTotalCounter(filter) { return async dispatch => { dispatch(fetchTotalCounterStart()); try { const { data: { payload } } = await entityModel.fetchTotalCounter(filter); dispatch(fetchTotalCounterSuccess(payload)); } catch (error) { dispatch(fetchTotalCounterError(error)); } }; } function fetchData(filter, dispatch) { dispatch(fetchTotalCounter(filter)); return entityModel.fetchData(filter); } function applyFilter(newFilter) { return async (dispatch, getStore) => { try { const store = getStore(); const currentStore = getCurrentStore(store);
जहां:
prefix
- Redux रिपॉजिटरी में इकाई उपसर्ग। यह "CATS_", "MICE_", आदि के रूप में एक स्ट्रिंग है।getCurrentStore
- एक चयनकर्ता जो Redux रिपॉजिटरी से इकाई पर वर्तमान डेटा लौटाता है।entityModel
- इकाई मॉडल वर्ग का एक उदाहरण। एक ओर, सर्वर के लिए एक अनुरोध बनाने के लिए मॉडल के माध्यम से एक एपीआई का उपयोग किया जाता है, दूसरी तरफ, कुछ जटिल (या ऐसा नहीं) डेटा प्रोसेसिंग लॉजिक वर्णित है।
इस प्रकार, यह कारखाना आपको लचीले ढंग से Redux रिपॉजिटरी में एक विशेष इकाई के डेटा और मापदंडों के प्रबंधन का वर्णन करने और इस इकाई के अनुरूप तालिका के साथ संबद्ध करने की अनुमति देता है।
चूंकि इस प्रणाली के प्रबंधन में बहुत सारी बारीकियां हैं, इसलिए थन जटिल, स्वैच्छिक, भ्रामक हो सकता है और इसमें दोहराव हो सकते हैं। उन्हें सरल बनाने के लिए, साथ ही कोड का पुन: उपयोग करने के लिए, जटिल थ्रक्स को सरल लोगों में तोड़ दिया जाता है और एक रचना में जोड़ा जाता है। इसके परिणामस्वरूप, अब यह पता चल सकता है कि एक ठग दूसरे को कॉल करता है, जो पहले से ही सामान्य applyFilter
कर सकता है (जैसे कि applyFilter
बंडल - उपरोक्त उदाहरण से fetchTotalCounter
)। और जब सभी मुख्य बिंदुओं को ध्यान में रखा गया था, और सभी आवश्यक थंक और एक्शन क्रिएटर्स का वर्णन किया गया था, तो एक्शन क्रिएटर फ़ंक्शन वाली फ़ाइल में कोड की ~ 1200 लाइनें थीं और महान स्क्वैक के साथ परीक्षण किया गया था। परीक्षण फ़ाइल में लगभग 1200 लाइनें भी थीं, लेकिन कवरेज सबसे अच्छा 40-50% था।
यहां, उदाहरण, निश्चित रूप से, बहुत सरल किया गया है, दोनों ठग की संख्या और उनके आंतरिक तर्क के संदर्भ में, लेकिन यह समस्या को प्रदर्शित करने के लिए पर्याप्त होगा।
ऊपर दिए गए उदाहरण में 2 प्रकार के थंक पर ध्यान दें:
fetchTotalCounter
- प्रेषण-यह केवल fetchTotalCounter
।applyFilter
- इसके से संबंधित applyFilter
के प्रेषण के अलावा ( applyFilterSuccess
, applyFilterError
), प्रेषण - यह भी एक और fetchTotalCounter
( fetchTotalCounter
) है।
हम थोड़ी देर बाद उनके पास लौट आएंगे।
यह सब निम्नानुसार परीक्षण किया गया था (फ्रेम का उपयोग जेस्ट का परीक्षण करने के लिए किया गया था):
import configureMockStore from 'redux-mock-store'; import thunk from 'redux-thunk'; import { actionsCreator } from '../actions'; describe('actionsCreator', () => { const defaultState = {}; const middlewares = [thunk]; const mockStore = configureMockStore(middlewares); const prefix = 'TEST_'; const getCurrentStore = () => defaultState; const entityModel = { fetchTotalCounter: jest.fn(), fetchData: jest.fn(), }; let actions; beforeEach(() => { actions = actionsCreator(prefix, getCurrentStore, entityModel); }); describe('fetchTotalCounter', () => { it('should dispatch correct actions on success', () => { const filter = {}; const payload = 0; const store = mockStore(defaultState); entityModel.fetchTotalCounter.mockResolvedValueOnce({ data: { payload }, }); const expectedActions = [ { type: `${prefix}FETCH_TOTAL_COUNTER_START` }, { type: `${prefix}FETCH_TOTAL_COUNTER_SUCCESS`, payload, } ]; return store.dispatch(actions.fetchTotalCounter(filter)).then(() => { expect(store.getActions()).toEqual(expectedActions); }); }); it('should dispatch correct actions on error', () => { const filter = {}; const error = {}; const store = mockStore(defaultState); entityModel.fetchTotalCounter.mockRejectedValueOnce(error); const expectedActions = [ { type: `${prefix}FETCH_TOTAL_COUNTER_START` }, { type: `${prefix}FETCH_TOTAL_COUNTER_ERROR`, error, } ]; return store.dispatch(actions.fetchTotalCounter(filter)).then(() => { expect(store.getActions()).toEqual(expectedActions); }); }); }); describe('applyFilter', () => { it('should dispatch correct actions on success', () => { const payload = {}; const counter = 0; const newFilter = {}; const store = mockStore(defaultState); entityModel.fetchData.mockResolvedValueOnce({ data: { payload } }); entityModel.fetchTotalCounter.mockResolvedValueOnce({ data: { payload: counter }, }); const expectedActions = [
जैसा कि आप देख सकते हैं, पहले प्रकार के थंक के परीक्षण में कोई समस्या नहीं है - आपको केवल एंटमॉडल मॉडल entityModel
को हुक करने की आवश्यकता है, लेकिन दूसरा प्रकार अधिक जटिल है - आपको पूरी श्रृंखला को थंक और संबंधित मॉडल विधियों के लिए डेटा पोंछना होगा। अन्यथा, परीक्षण डेटा के विघटन ( {डेटा: {पेलोड}} ) पर पड़ेगा, और यह स्पष्ट रूप से या अंतर्निहित रूप से भी हो सकता है (यह ऐसा था कि परीक्षण सफलतापूर्वक पारित हो गया, लेकिन सावधानीपूर्वक शोध में यह देखा गया कि दूसरे या तीसरे में इस श्रृंखला का लिंक लॉक किए गए डेटा की कमी के कारण परीक्षण में एक गिरावट थी)। यह भी बुरा है कि व्यक्तिगत कार्यों की इकाई परीक्षण एक प्रकार के एकीकरण में बदल जाते हैं, और निकटता से संबंधित हो जाते हैं।
सवाल यह उठता है: क्यों applyFilter
फंक्शन में यह जाँचें कि अगर इसके लिए पहले से ही अलग-अलग विस्तृत परीक्षण लिखे जा चुके हैं तो fetchTotalCounter
फ़ंक्शन कैसे fetchTotalCounter
है? मैं दूसरे प्रकार के थंक के परीक्षण को और अधिक स्वतंत्र कैसे बना सकता हूं? यह परीक्षण करने का अवसर प्राप्त करने के लिए बहुत अच्छा होगा कि fetchTotalCounter
(इस मामले में fetchTotalCounter
) को सही मापदंडों के साथ कहा जाता है , और इसके लिए सही तरीके से काम करने के लिए मोक्स की देखभाल करने की कोई आवश्यकता नहीं होगी।
लेकिन यह कैसे करें? स्पष्ट निर्णय दिमाग में आता है: fetchData फ़ंक्शन को हुक करने के लिए, जिसे applyFilter
में कहा जाता है, या fetchTotalCounter
(क्योंकि अक्सर दूसरे fetchTotalCounter
को सीधे कहा जाता है, और fetchData
जैसे किसी अन्य फ़ंक्शन के माध्यम से नहीं) fetchData
।
चलो इसे आजमाएँ। उदाहरण के लिए, हम केवल एक सफल स्क्रिप्ट को बदल देंगे।
describe('applyFilter', () => { it('should dispatch correct actions on success', () => { const payload = {}; - const counter = 0; const newFilter = {}; const store = mockStore(defaultState); - entityModel.fetchData.mockResolvedValueOnce({ data: { payload } }); - entityModel.fetchTotalCounter.mockResolvedValueOnce({ - data: { payload: counter }, - }); + const fetchData = jest.spyOn(actions, 'fetchData'); + // or fetchData.mockImplementationOnce(Promise.resolve({ data: { payload } })); + fetchData.mockResolvedValueOnce({ data: { payload } }); const expectedActions = [ - // Total counter actions - { type: `${prefix}FETCH_TOTAL_COUNTER_START` }, - { - type: `${prefix}FETCH_TOTAL_COUNTER_SUCCESS`, - payload: counter, - }, - // apply filter actions { type: `${prefix}APPLY_FILTER_SUCCESS`, payload, } ]; return store.dispatch(actions.applyFilter(newFilter)).then(() => { expect(store.getActions()).toEqual(expectedActions); + expect(actions.fetchTotalCounter).toBeCalledWith(newFilter); }); }); });
यहाँ, jest.spyOn
विधि निम्नलिखित कार्यान्वयन की जगह (और शायद बिलकुल) बदल देती है:
actions.fetchData = jest.fn(actions.fetchData);
यह हमें फ़ंक्शन को "मॉनिटर" करने और यह समझने की अनुमति देता है कि क्या इसे बुलाया गया था और किन मापदंडों के साथ।
हमें निम्न त्रुटि मिलती है:
Difference: - Expected + Received Array [ Object { - "payload": Object {}, - "type": "TEST_APPLY_FILTER_SUCCESS", + "type": "TEST_FETCH_TOTAL_COUNTER_START", }, + Object { + "error": [TypeError: Cannot read property 'data' of undefined], + "type": "TEST_FETCH_TOTAL_COUNTER_ERROR", + }, + Object { + "error": [TypeError: Cannot read property 'data' of undefined], + "type": "TEST_APPLY_FILTER_ERROR", + }, ]
अजीब बात है, हम अपने कार्यान्वयन को fetchData
करते हुए, fetchData फ़ंक्शन को छिपाते हैं
fetchData.mockResolvedValueOnce({ data: { payload } })
लेकिन फ़ंक्शन पहले की तरह ही काम करता है, यानी मॉक ने काम नहीं किया! आइए इसे अलग तरीके से आजमाएं।
describe('applyFilter', () => { it('should dispatch correct actions on success', () => { const payload = {}; - const counter = 0; const newFilter = {}; const store = mockStore(defaultState); entityModel.fetchData.mockResolvedValueOnce({ data: { payload } }); - entityModel.fetchTotalCounter.mockResolvedValueOnce({ - data: { payload: counter }, - }); + const fetchTotalCounter = jest.spyOn(actions, 'fetchTotalCounter'; + fetchTotalCounter.mockImplementation(() => {}); const expectedActions = [ - // Total counter actions - { type: `${prefix}FETCH_TOTAL_COUNTER_START` }, - { - type: `${prefix}FETCH_TOTAL_COUNTER_SUCCESS`, - payload: counter, - }, - // apply filter actions { type: `${prefix}APPLY_FILTER_SUCCESS`, payload, } ]; return store.dispatch(actions.applyFilter(newFilter)).then(() => { expect(store.getActions()).toEqual(expectedActions); + expect(actions.fetchTotalCounter).toBeCalledWith(newFilter); }); }); });
हमें ठीक वैसी ही त्रुटि मिलती है। किसी कारण से, हमारे मोक्स कार्यों के मूल कार्यान्वयन को प्रतिस्थापित नहीं करते हैं।
इस समस्या का अपने आप पता लगाने और इंटरनेट पर कुछ जानकारी प्राप्त करने के बाद, मैंने महसूस किया कि यह समस्या न केवल मेरे पास मौजूद है, और इसे (मेरी राय में) काफी गंभीर रूप से हल किया गया है। इसके अलावा, इन स्रोतों में वर्णित उदाहरण तब तक अच्छे हैं जब तक वे किसी ऐसी चीज का हिस्सा नहीं बन जाते हैं जो उन्हें एक प्रणाली में जोड़ता है (हमारे मामले में, यह मापदंडों के साथ एक कारखाना है)।
जेनकिंस पाइपलाइन में हमारी परियोजना पर सोनारक्यूब से एक कोड की जांच है, जिसमें संशोधित फ़ाइलों को कवर करने की आवश्यकता है (जो मर्ज / पुल अनुरोध में हैं) > 60%
। चूंकि इस कारखाने का कवरेज, जैसा कि पहले कहा गया था, असंतोषजनक था, और इस तरह की फाइल को कवर करने की बहुत आवश्यकता थी केवल अवसाद का कारण था, इसके साथ कुछ किया जाना था, अन्यथा नई कार्यक्षमता का वितरण समय के साथ धीमा हो सकता था। केवल एक ही मर्ज / पुल अनुरोध में अन्य फाइलों (घटकों, कार्यों) के परीक्षण कवरेज को बचाया, ताकि वांछित कवरेज तक% कवरेज तक पहुंच सके, लेकिन, वास्तव में, यह एक समाधान था, समस्या का समाधान नहीं। और एक ठीक क्षण, स्प्रिंट में थोड़ा समय आवंटित करने के बाद, मैं सोचने लगा कि इस समस्या को कैसे हल किया जा सकता है।
समस्या नंबर 1 को हल करने का प्रयास। मैंने Redux-Saga के बारे में कुछ सुना ...
... और उन्होंने मुझे बताया कि इस मिडलवेयर का उपयोग करते समय परीक्षण बहुत सरल है।
वास्तव में, यदि आप प्रलेखन को देखते हैं , तो आपको आश्चर्य होता है कि कोड का परीक्षण कितना सरल है। रस स्वयं इस तथ्य में निहित है कि इस दृष्टिकोण के साथ इस तथ्य पर कोई समस्या नहीं है कि कुछ गाथा एक और गाथा कह सकती है - हम गीले हो सकते हैं और मिडलवेयर ( put
, take
, आदि) द्वारा प्रदान किए गए कार्यों को "सुन" सकते हैं, और सत्यापित करें कि उन्हें बुलाया गया था (और सही मापदंडों के साथ कहा जाता है)। यही है, इस मामले में, फ़ंक्शन सीधे दूसरे फ़ंक्शन तक नहीं पहुंचता है, लेकिन लाइब्रेरी से एक फ़ंक्शन को संदर्भित करता है, जो तब अन्य आवश्यक फ़ंक्शन / सागा कहता है।
"इस मिडलवेयर की कोशिश क्यों नहीं की गई?" मैंने सोचा, और काम पर लग गया। उन्होंने जीरा में एक तकनीकी इतिहास शुरू किया, इसमें कई कार्य बनाए (अनुसंधान से लेकर इस पूरे सिस्टम की वास्तुकला के कार्यान्वयन और विवरण तक), "गो-फॉरवर्ड" प्राप्त किया और एक नए दृष्टिकोण के साथ वर्तमान प्रणाली की एक न्यूनतम प्रतिलिपि बनाना शुरू किया।
शुरुआत में, सब कुछ ठीक चला। डेवलपर्स में से एक की सलाह पर, नए दृष्टिकोण पर डेटा लोड करने और त्रुटि से निपटने के लिए एक वैश्विक गाथा बनाना संभव था। हालांकि, कुछ बिंदु पर परीक्षण के साथ समस्याएं थीं (जो, संयोग से, अब तक हल नहीं हुई हैं)। मैंने सोचा था कि यह वर्तमान में उपलब्ध सभी परीक्षणों को नष्ट कर सकता है और बगों का एक समूह पैदा कर सकता है, इसलिए मैंने इस कार्य पर काम को स्थगित करने का फैसला किया जब तक कि समस्या का कुछ समाधान नहीं हुआ, और उत्पादक कार्यों के लिए नीचे उतर गया।
एक या दो महीने बीत गए, कोई समाधान नहीं मिला, और कुछ बिंदु पर, उन लोगों के साथ चर्चा की। इस कार्य में अग्रणी (अनुपस्थित) प्रगति, उन्होंने प्रोजेक्ट में Redux-Saga के कार्यान्वयन को छोड़ने का फैसला किया, क्योंकि उस समय तक यह श्रम लागत और बग की संभावित संख्या के मामले में बहुत महंगा हो गया था। इसलिए हम आखिरकार Redux Thunk का उपयोग करने के लिए तैयार हो गए।
समस्या नंबर 2 को हल करने का प्रयास। थंक मॉड्यूल
आप सभी फ़ाइलों को अलग-अलग फ़ाइलों में सॉर्ट कर सकते हैं, और उन फ़ाइलों में जहां एक थंक दूसरे (आयातित) को कॉल करता है, आप या तो jest.mock
विधि का उपयोग करके या उसी jest.spyOn
का उपयोग करके इस आयात को मिटा सकते हैं। इस प्रकार, हम यह सत्यापित करने के उपरोक्त कार्य को प्राप्त करेंगे कि कुछ बाहरी ठग को आवश्यक मापदंडों के साथ बुलाया गया था , इसके लिए मोक की चिंता किए बिना। इसके अलावा, सभी फंक को उनके कार्यात्मक उद्देश्य के अनुसार तोड़ना बेहतर होगा, ताकि उन सभी को एक ढेर में न रखा जाए। तो ऐसी तीन प्रजातियों को प्रतिष्ठित किया गया:
- टेम्प्लेट -
templates
साथ काम करने से संबंधित। - फ़िल्टर के साथ काम करने से संबंधित (छंटाई, कॉलम प्रदर्शित करना) -
filter
। - तालिका के साथ काम करने से संबंधित (स्क्रॉल करते समय नया डेटा लोड करना, चूंकि तालिका में एक वर्चुअल स्क्रॉल है, मेटा-डेटा लोड करना, तालिका में रिकॉर्ड के काउंटर द्वारा डेटा लोड करना, आदि) -
table
।
निम्नलिखित फ़ोल्डर और फ़ाइल संरचना प्रस्तावित थी:
src/ |-- store/ | |-- filter/ | | |-- actions/ | | | |-- thunks/ | | | | |-- __tests__/ | | | | | |-- applyFilter.test.js | | | | |-- applyFilter.js | | | |-- actionCreators.js | | | |-- index.js | |-- table/ | | |-- actions/ | | | |-- thunks/ | | | | |-- __tests__/ | | | | | |-- fetchData.test.js | | | | | |-- fetchTotalCounter.test.js | | | | |-- fetchData.js | | | | |-- fetchTotalCounter.js | | | |-- actionCreators.js | | | |-- index.js (main file with actionsCreator)
इस वास्तुकला का एक उदाहरण यहाँ है ।
ApplyFilter के लिए परीक्षण फ़ाइल में, आप देख सकते हैं कि हम उस लक्ष्य तक पहुँच चुके हैं जिसके लिए हम प्रयास कर रहे थे - आप fetchData
/ fetchTotalCounter
का सही संचालन बनाए रखने के लिए मोक नहीं लिख सकते हैं। लेकिन किस कीमत पर ...
import { applyFilterSuccess, applyFilterError } from '../'; import { fetchData } from '../../../table/actions';
import * as filterActions from './filter/actions'; import * as tableActions from './table/actions'; export const actionsCreator = (prefix, getCurrentStore, entityModel) => { return { fetchTotalCounterStart: tableActions.fetchTotalCounterStart(prefix), fetchTotalCounterSuccess: tableActions.fetchTotalCounterSuccess(prefix), fetchTotalCounterError: tableActions.fetchTotalCounterError(prefix), applyFilterSuccess: filterActions.applyFilterSuccess(prefix), applyFilterError: filterActions.applyFilterError(prefix), fetchTotalCounter: tableActions.fetchTotalCounter(prefix, entityModel), fetchData: tableActions.fetchData(prefix, entityModel), applyFilter: filterActions.applyFilter(prefix, getCurrentStore, entityModel) }; };
हमें कोड के दोहराव और एक-दूसरे पर बहुत मजबूत निर्भरता के साथ परीक्षणों की माप के लिए भुगतान करना पड़ा। कॉल श्रृंखला में मामूली बदलाव से भारी रिफैक्टिंग हो जाएगी।
ऊपर दिए गए उदाहरण में, table
और filter
के लिए उदाहरण दिए गए उदाहरणों की स्थिरता बनाए रखने के लिए प्रदर्शित किए गए थे। वास्तव में, रीफैक्टरिंग को templates
साथ शुरू किया गया था (जैसा कि यह सरल हो गया था), और वहां, ऊपर रिफैक्टरिंग के अलावा, टेम्प्लेट के साथ काम करने की अवधारणा को थोड़ा बदल दिया गया था। एक धारणा के रूप में, यह स्वीकार किया गया था कि एक पृष्ठ (एक तालिका की तरह) पर केवल एक ही पैनल के टेम्पलेट हो सकते हैं। उस समय यह सिर्फ और सिर्फ यही था चूक धारणा ने हमें prefix
से छुटकारा पाकर कोड को थोड़ा सरल बनाने की अनुमति दी।
मुख्य विकास शाखा में परिवर्तन किए जाने और परीक्षण किए जाने के बाद, मैं शांत आत्मा के साथ छुट्टी पर गया ताकि लौटने के बाद बाकी कोड को एक नए दृष्टिकोण में स्थानांतरित करना जारी रख सकूं।
छुट्टी से लौटने के बाद, मुझे यह जानकर आश्चर्य हुआ कि मेरे परिवर्तन वापस किए गए थे। यह पता चला कि एक पृष्ठ दिखाई दिया, जिस पर कई स्वतंत्र तालिकाएं हो सकती हैं, अर्थात्, पहले की गई धारणा ने सब कुछ तोड़ दिया। अतः सभी कार्य व्यर्थ हो गए…
खैर, लगभग। वास्तव में, सभी समान कार्यों को फिर से करना संभव होगा (मर्ज / पुल अनुरोध का लाभ गायब नहीं हुआ, लेकिन इतिहास में रहा), टेम्पलेट आर्किटेक्चर के दृष्टिकोण को अपरिवर्तित छोड़ दिया, और केवल थंक-एस को व्यवस्थित करने के दृष्टिकोण को बदल दिया। लेकिन यह दृष्टिकोण अभी भी अपनी सुसंगतता और जटिलता के कारण आत्मविश्वास को प्रेरित नहीं करता था। इसे वापस करने की कोई इच्छा नहीं थी, हालांकि इसने परीक्षण के साथ संकेतित समस्या को हल किया। यह कुछ और, सरल और अधिक विश्वसनीय के साथ आने के लिए आवश्यक था।
समस्या संख्या 3 को हल करने का प्रयास। वह जो खोजता है वह पा लेगा
वैश्विक रूप से यह देखते हुए कि थंक के लिए परीक्षण कैसे लिखे जाते हैं, मैंने देखा कि कैसे और कितनी आसानी से बिना किसी समस्या के तरीके (वास्तव में, ऑब्जेक्ट फ़ील्ड) entityModel
।
तब यह विचार सामने आया: क्यों न एक ऐसा वर्ग बनाया जाए जिसके तरीके ठग और एक्शन क्रिएटर हों? कारखाने के लिए पारित किए गए पैरामीटर इस वर्ग के कंस्ट्रक्टर को पारित किए जाएंगे और इसके माध्यम से सुलभ होंगे। आप तुरंत एक्शन क्रिएटर्स के लिए एक अलग वर्ग और थंक के लिए एक अलग से एक छोटा अनुकूलन बना सकते हैं, और फिर दूसरे से एक वारिस कर सकते हैं। इस प्रकार, ये वर्ग एक के रूप में काम करेंगे (जब वारिस वर्ग का एक उदाहरण बनाते हैं), लेकिन एक ही समय में प्रत्येक वर्ग को व्यक्तिगत रूप से पढ़ना, समझना और परीक्षण करना आसान होगा।
इस दृष्टिकोण को प्रदर्शित करने वाला एक कोड है।
आइए प्रत्येक दिखाई और बदली गई फ़ाइलों में से एक पर अधिक विस्तार से विचार करें।
export class FilterActionCreators { constructor(config) { this.prefix = config.prefix; } applyFilterSuccess = payload => ({ type: `${this.prefix}APPLY_FILTER_SUCCESS`, payload, }); applyFilterError = error => ({ type: `${this.prefix}APPLY_FILTER_ERROR`, error, }); }
FilterActions.js
फ़ाइल में, FilterActions.js
FilterActionCreators
वर्ग से इनहेरिट करते हैं और इस क्लास की एक विधि के रूप में thunk applyFilter
को परिभाषित करते हैं। इस स्थिति में, एक्शन applyFilterSuccess
और applyFilterError
इसके माध्यम से इसमें उपलब्ध होगा:
import { FilterActionCreators } from '/FilterActionCreators';
- सभी थंक और एक्शन
FilterActions
साथ मुख्य फाइल में , हम FilterActions
क्लास का एक उदाहरण बनाते हैं, इसे आवश्यक कॉन्फ़िगरेशन ऑब्जेक्ट पास करते हैं। फ़ंक्शंस निर्यात करते समय ( applyFilter
फ़ंक्शन के बहुत अंत में), applyFilter
को पास करने के लिए applyFilter
विधि को ओवरराइड करना न भूलें:
+ import { FilterActions } from './filter/actions/FilterActions'; - // selector - const getFilter = store => store.filter; export const actionsCreator = (prefix, getCurrentStore, entityModel) => { + const config = { prefix, getCurrentStore, entityModel }; + const filterActions = new FilterActions(config); /* --- ACTIONS BLOCK --- */ function fetchTotalCounterStart() { return { type: `${prefix}FETCH_TOTAL_COUNTER_START` }; } function fetchTotalCounterSuccess(payload) { return { type: `${prefix}FETCH_TOTAL_COUNTER_SUCCESS`, payload }; } function fetchTotalCounterError(error) { return { type: `${prefix}FETCH_TOTAL_COUNTER_ERROR`, error }; } - function applyFilterSuccess(payload) { - return { type: `${prefix}APPLY_FILTER_SUCCESS`, payload }; - } - - function applyFilterError(error) { - return { type: `${prefix}APPLY_FILTER_ERROR`, error }; - } /* --- THUNKS BLOCK --- */ function fetchTotalCounter(filter) { return async dispatch => { dispatch(fetchTotalCounterStart()); try { const { data: { payload } } = await entityModel.fetchTotalCounter(filter); dispatch(fetchTotalCounterSuccess(payload)); } catch (error) { dispatch(fetchTotalCounterError(error)); } }; } function fetchData(filter, dispatch) { dispatch(fetchTotalCounter(filter)); return entityModel.fetchData(filter); } - function applyFilter(newFilter) { - return async (dispatch, getStore) => { - try { - const store = getStore(); - const currentStore = getCurrentStore(store); - // 'getFilter' comes from selectors. - const filter = newFilter || getFilter(currentStore); - const { data: { payload } } = await fetchData(filter, dispatch); - - dispatch(applyFilterSuccess(payload)); - } catch (error) { - dispatch(applyFilterError(error)); - } - }; - } return { fetchTotalCounterStart, fetchTotalCounterSuccess, fetchTotalCounterError, - applyFilterSuccess, - applyFilterError, fetchTotalCounter, fetchData, - applyFilter + ...filterActions, + applyFilter: filterActions.applyFilter({ fetchData }), }; };
- परीक्षण कार्यान्वयन और पढ़ने में दोनों आसान हो गए हैं:
import { FilterActions } from '../FilterActions'; describe('FilterActions', () => { const prefix = 'TEST_'; const getCurrentStore = store => store; const entityModel = {}; const config = { prefix, getCurrentStore, entityModel }; const actions = new FilterActions(config); const dispatch = jest.fn(); beforeEach(() => { dispatch.mockClear(); }); describe('applyFilter', () => { const getStore = () => ({}); const newFilter = {}; it('should dispatch correct actions on success', async () => { const payload = {}; const fetchData = jest.fn().mockResolvedValueOnce({ data: { payload } }); const applyFilterSuccess = jest.spyOn(actions, 'applyFilterSuccess'); await actions.applyFilter({ fetchData })(newFilter)(dispatch, getStore); expect(fetchData).toBeCalledWith(newFilter, dispatch); expect(applyFilterSuccess).toBeCalledWith(payload); }); it('should dispatch correct actions on error', async () => { const error = {}; const fetchData = jest.fn().mockRejectedValueOnce(error); const applyFilterError = jest.spyOn(actions, 'applyFilterError'); await actions.applyFilter({ fetchData })(newFilter)(dispatch, getStore); expect(fetchData).toBeCalledWith(newFilter, dispatch); expect(applyFilterError).toBeCalledWith(error); }); }); });
सिद्धांत रूप में, परीक्षणों में, आप अंतिम जांच को इस तरह से बदल सकते हैं:
- expect(applyFilterSuccess).toBeCalledWith(payload); + expect(dispatch).toBeCalledWith(applyFilterSuccess(payload)); - expect(applyFilterError).toBeCalledWith(error); + expect(dispatch).toBeCalledWith(applyFilterError(error));
तब उन्हें jest.spyOn
साथ jest.spyOn
करने की कोई आवश्यकता नहीं होगी। , , . thunk, . , ...
, , , -: , thunk- action creator- , , . , . actionsCreator
- , :
import { FilterActions } from './filter/actions/FilterActions'; import { TemplatesActions } from './templates/actions/TemplatesActions'; import { TableActions } from './table/actions/TableActions'; export const actionsCreator = (prefix, getCurrentStore, entityModel) => { const config = { prefix, getCurrentStore, entityModel }; const filterActions = new FilterActions(config); const templatesActions = new TemplatesActions(config); const tableActions = new TableActions(config); return { ...filterActions, ...templatesActions, ...tableActions, }; };
. filterActions
templatesActions
tableActions
, , , filterActions
? , . . - , , .
. , back-end ( Java), . , Java/Spring , . - ?
:
- thunk-
setDependencies
, — dependencies
:
export class FilterActions extends FilterActionCreators { constructor(config) { super(config); this.getCurrentStore = config.getCurrentStore; this.entityModel = config.entityModel; } + setDependencies = dependencies => { + this.dependencies = dependencies; + };
import { FilterActions } from './filter/actions/FilterActions'; import { TemplatesActions } from './templates/actions/TemplatesActions'; import { TableActions } from './table/actions/TableActions'; export const actionsCreator = (prefix, getCurrentStore, entityModel) => { const config = { prefix, getCurrentStore, entityModel }; const filterActions = new FilterActions(config); const templatesActions = new TemplatesActions(config); const tableActions = new TableActions(config); + const actions = { + ...filterActions, + ...templatesActions, + ...tableActions, + }; + + filterActions.setDependencies(actions); + templatesActions.setDependencies(actions); + tableActions.setDependencies(actions); + return actions; - return { - ...filterActions, - ...templatesActions, - ...tableActions, - }; };
applyFilter = newFilter => { const { fetchData } = this.dependencies; return async (dispatch, getStore) => { try { const store = getStore(); const currentStore = this.getCurrentStore(store); const filter = newFilter || getFilter(currentStore); const { data: { payload } } = await fetchData(filter, dispatch);
, applyFilter
, - this.dependencies
. , .
import { FilterActions } from '../FilterActions'; describe('FilterActions', () => { const prefix = 'TEST_'; const getCurrentStore = store => store; const entityModel = {}; + const dependencies = { + fetchData: jest.fn(), + }; const config = { prefix, getCurrentStore, entityModel }; const actions = new FilterActions(config); + actions.setDependencies(dependencies); const dispatch = jest.fn(); beforeEach(() => { dispatch.mockClear(); }); describe('applyFilter', () => { const getStore = () => ({}); const newFilter = {}; it('should dispatch correct actions on success', async () => { const payload = {}; - const fetchData = jest.fn().mockResolvedValueOnce({ data: { payload } }); + dependencies.fetchData.mockResolvedValueOnce({ data: { payload } }); const applyFilterSuccess = jest.spyOn(actions, 'applyFilterSuccess'); - await actions.applyFilter({ fetchData })(newFilter)(dispatch, getStore); + await actions.applyFilter(newFilter)(dispatch, getStore); - expect(fetchData).toBeCalledWith(newFilter, dispatch); + expect(dependencies.fetchData).toBeCalledWith(newFilter, dispatch); expect(applyFilterSuccess).toBeCalledWith(payload); }); it('should dispatch correct actions on error', async () => { const error = {}; - const fetchData = jest.fn().mockRejectedValueOnce(error); + dependencies.fetchData.mockRejectedValueOnce(error); const applyFilterError = jest.spyOn(actions, 'applyFilterError'); - await actions.applyFilter({ fetchData })(newFilter)(dispatch, getStore); + await actions.applyFilter(newFilter)(dispatch, getStore); - expect(fetchData).toBeCalledWith(newFilter, dispatch); + expect(dependencies.fetchData).toBeCalledWith(newFilter, dispatch); expect(applyFilterError).toBeCalledWith(error); }); }); });
.
, , :
import { FilterActions } from './filter/actions/FilterActions'; import { TemplatesActions } from './templates/actions/TemplatesActions'; import { TableActions } from './table/actions/TableActions'; - export const actionsCreator = (prefix, getCurrentStore, entityModel) => { + export const actionsCreator = (prefix, getCurrentStore, entityModel, ExtendedActions) => { const config = { prefix, getCurrentStore, entityModel }; const filterActions = new FilterActions(config); const templatesActions = new TemplatesActions(config); const tableActions = new TableActions(config); + const extendedActions = ExtendedActions ? new ExtendedActions(config) : undefined; const actions = { ...filterActions, ...templatesActions, ...tableActions, + ...extendedActions, }; filterActions.setDependencies(actions); templatesActions.setDependencies(actions); tableActions.setDependencies(actions); + if (extendedActions) { + extendedActions.setDependencies(actions); + } return actions; };
export class ExtendedActions { constructor(config) { this.getCurrentStore = config.getCurrentStore; this.entityModel = config.entityModel; } setDependencies = dependencies => { this.dependencies = dependencies; };
, , :
- , .
- .
- , , thunk- .
- , , thunk-/action creator- 99-100%.
action creator- ( filter
, templates
, table
), reducer- - , , actionsCreator
- , reducer- ~400-500 .
:
import isNull from 'lodash/isNull'; import { getDefaultState } from '../getDefaultState'; import { templatesReducerConfigurator } from 'src/store/templates/reducers/templatesReducerConfigurator'; import { filterReducerConfigurator } from 'src/store/filter/reducers/filterReducerConfigurator'; import { tableReducerConfigurator } from 'src/store/table/reducers/tableReducerConfigurator'; export const createTableReducer = ( prefix, initialState = getDefaultState(), entityModel, ) => { const config = { prefix, initialState, entityModel }; const templatesReducer = templatesReducerConfigurator(config); const filterReducer = filterReducerConfigurator(config); const tableReducer = tableReducerConfigurator(config); return (state = initialState, action) => { const templatesState = templatesReducer(state, action); if (!isNull(templatesState)) { return templatesState; } const filterState = filterReducer(state, action); if (!isNull(filterState)) { return filterState; } const tableState = tableReducer(state, action); if (!isNull(tableState)) { return tableState; } return state; }; };
tableReducerConfigurator
( ):
export const tableReducerConfigurator = ({ prefix, entityModel }) => { return (state, action) => { switch (action.type) { case `${prefix}FETCH_TOTAL_COUNTER_START`: { return { ...state, isLoading: true, error: null, }; } case `${prefix}FETCH_TOTAL_COUNTER_SUCCESS`: { return { ...state, isLoading: false, counter: action.payload, }; } case `${prefix}FETCH_TOTAL_COUNTER_ERROR`: { return { ...state, isLoading: false, error: action.error, }; } default: { return null; } } }; };
:
reducerConfigurator
- action type-, «». action type case, null ().reducerConfigurator
- , null , reducerConfigurator
- !null . , reducerConfigurator
- case, reducerConfigurator
-.- ,
reducerConfigurator
- case- action type-, ( reducer-).
, actionsCreator
-, , , , .
, !
, Redux Thunk.
, Redux Thunk . , .