6

在我們的JavaScript開發團隊中,我們已經接受了編寫純功能代碼的減少/反應風格。但是,我們似乎無法單元測試我們的代碼。請看下面的例子:如何單獨測試一個純函數調用樹?

function foo(data) { 
    return process({ 
     value: extractBar(data.prop1), 
     otherValue: extractBaz(data.prop2.someOtherProp) 
    }); 
} 

此函數調用取決於調用processextractBarextractBaz,其中的每一個可以調用其他功能。在一起,他們可能需要一個非平凡的模擬data參數來構建測試。

我們是否應該接受制作這樣一個模擬對象的必要性,並且在測試中真的這樣做,我們很快就會發現我們有難以閱讀和維護的測試用例。此外,它很可能導致一遍又一遍地測試相同的東西,因爲大概也應該寫成process,extractBarextractBaz的單元測試。通過foo接口對這些功能實現的每個可能的邊緣情況進行測試是笨拙的。


我們有幾個解決方案,但不是真的喜歡任何,因爲它們都不像我們以前見過的模式。

解決方案1:

function foo(data, deps = defaultDeps) { 
    return deps.process({ 
     value: deps.extractBar(data.prop1), 
     otherValue: deps.extractBaz(data.prop2.someOtherProp) 
    }); 
} 

解決方案2:

function foo(
    data, 
    processImpl = process, 
    extractBarImpl = extractBar, 
    extractBazImpl = extractBaz 
) { 
    return process({ 
     value: extractBar(data.prop1), 
     otherValue: extractBaz(data.prop2.someOtherProp) 
    }); 
} 

溶液2非常迅速地調用相關函數的數量的增加污染foo方法簽名。

解決方案3:

只要接受一個事實,即foo是一個複雜的複合操作,並測試它作爲一個整體。所有缺點都適用。


請提出其他可能性。我想這是功能編程社區必須以某種方式解決的問題。

回答

6

您可能不需要您考慮過的任何解決方案。函數式編程和命令式編程之間的區別之一是函數式應該產生更易於推理的代碼。不僅僅是在精神上「玩編譯器」,模擬給定的輸入集會發生什麼,而是從更多的數學意義上推理你的代碼。

例如,單元測試的目標是測試「可能破壞的所有東西」。看看你發佈的第一個代碼片段,我們可以推理這個函數,並問「這個函數怎麼會中斷?」這是一個簡單的函數,我們根本不需要玩編譯器。如果process()函數未能爲給定的一組輸入返回正確的值,即它返回的結果無效或者拋出異常,那麼函數會中斷。這又意味着我們還需要測試extractBar()extractBaz()是否返回正確的結果,以便將正確的值傳遞給process()

所以真的,你只需要測試foo()是否引發意外的異常,因爲它是所有的呼叫process(),你應該在它自己的一套單元測試來測試process()。與extractBar()extractBaz()一樣。如果這兩個函數在給定有效輸入時返回正確的結果,則它們將傳遞正確的值到process(),並且如果process()在給定有效輸入時產生正確的結果,則foo()也將返回正確的結果。

你可能會說:「那麼論點呢?如果它從data結構中提取錯誤的值會怎麼樣?」但那真的能打破嗎?如果我們看看這個函數,它使用核心JS點符號來訪問對象的屬性。我們不在我們的應用程序的單元測試中測試語言本身的核心功能。我們可以看看代碼,它是基於硬編碼的對象屬性訪問提取值,並繼續我們的其他測試。

這並不是說你可以扔掉你的單元測試,但是很多經驗豐富的函數程序員發現他們需要很少的測試,因爲你只需要測試可能破壞的東西,以及函數式編程減少易碎物品的數量,以便您可以將測試集中在真正處於風險中的零部件上。順便說一句,如果你在處理複雜的數據,並且擔心即使使用FP,也可能難以推斷出所有可能的排列組合,那麼你可能需要研究生成測試。我認爲那裏有幾個JS庫。

相關問題