2016-07-29 62 views
8

我有一個功能負責收集一堆配置,並從所有這些部分中做出更大的配置。因此,它基本上是:如何使此功能可測試?

let applyUpdate updateData currentState = 
    if not (someConditionAbout updateData) then 
     log (SomeError) 

    let this = getThis updateData currentState.Thingy 
    let that = getThat updateData currentState.Thingy 
    let andThat = createThatThing that this updateData 

    // blablablablablabla 

    { currentState with 
     This = this 
     That = that 
     AndThat = andThat 
     // etc. } 

我現在有getThisgetThatcreateThatThing,但不適用於applyUpdate單元測試。我不想重新測試getThis等等在做什麼,我只想測試applyUpdate特定的邏輯,僅僅是存根getThis。在面向對象的風格中,這些將通過依賴注入通過接口傳遞。在實用的風格,我不確定要如何進行:

// This is the function called by tests 
let applyUpdateTestable getThisFn getThatFn createThatThingfn etc updateData currentState = 
    if not (someConditionAbout updateData) then 
     log (SomeError) 

    let this = getThisFn updateData currentState.Thingy 
    // etc 

    { currentState with 
     This = this 
     // etc. } 

// This is the function that is actually called by client code 
let applyUpdate = applyUpdateTestable getThis getThat etc 

這似乎庶子注射液在功能上等同,但除此之外,我主要關心的是:

  • 現在我的代碼更難以遵循,因爲你不能只將F12(去 定義)轉換成函數;這個問題也存在於 OO依賴注入中,但是通過加工來緩解(即Resharper Go To Implementation)。
  • 我測試的功能是不是技術上的一個由生產代碼調用(有可能是在貼圖錯誤)
  • 我甚至不看一個好名字的功能
  • 的說,「可測試」版本
  • 我污染的一切

如何應對函數式編程這些問題的重複定義的模塊?

回答

9

你說:

在面向對象的風格,這些將通過通過依賴注入的接口傳遞。

並且在FP中使用相同的方法,但不是通過對象構造函數注入,而是將「注入」作爲函數的參數。

因此,您的applyUpdateTestable正處於正確的軌道上,除了這也將用作真實代碼,而不僅僅是可測試的代碼。

例如,這裏是在通過了三個額外的依賴關係的功能:

module Core = 
    let applyUpdate getThisFn getThatFn createThatThingfn updateData currentState = 
     if not (someConditionAbout updateData) then 
      log (SomeError) 

     let this = getThisFn updateData currentState.Thingy 
     // etc 

     { currentState with 
      This = this 
      // etc. } 

然後,在「生產」的代碼,你注入真正的依賴關係:

module Production =   
    let applyUpdate updateData currentState = 
     Core.applyUpdate Real.getThis Real.getThat Real.createThatThingfn updateData currentState 

或者更簡單,使用部分應用程序:

module Production =   
    let applyUpdate = 
     Core.applyUpdate Real.getThis Real.getThat Real.createThatThing 

並在測試版本中,您注入嘲笑或存根代替:

module Test =  
    let applyUpdate = 
     Core.applyUpdate Mock.getThis Mock.getThat Mock.createThatThing 

在上面的「生產」例如,我靜態硬編碼在Real函數的依賴關係,但是可替換地, 就像用OO風格依賴注入,生產applyUpdate可以創建由一些頂級協調員 然後傳遞給需要它的函數。

這回答你的問題,我希望:

  • 相同的核心代碼同時用於生產和檢測
  • 如果靜態硬編碼的依賴,你仍然可以使用F12鑽到他們。

這種方法有更復雜的版本,例如「Reader」單元,但上面的代碼是最簡單的方法。

Mark Seemann在這個主題上有很多不錯的帖子,比如Integration TestingSOLID: the next step is FunctionalPorts and Adapters

+0

我愛你的網站btw!我仍然對此有一些擔憂: - 您仍然無法從寫入邏輯的位置(「核心」)開始F12。現在,您已經將具體功能移到了不同​​的模塊中,因此更難以遵循。但你確實解決了污染問題。 - 我很好奇「標準」做法是什麼;用這種風格編寫的開源F#代碼的任何例子? – Asik

+0

謝謝!確實,你不知道傳遞給核心函數的函數是什麼(就像在OO中傳遞接口時一樣)。我從來沒有理解自己需要「goto實現」,但是你可以在覈心'applyUpdate'實現上「找到所有引用」,然後在使用它的調用位置找到傳遞給哪些函數它。這很好,我希望:) – Grundoon

+0

關於良好實踐的例子,機會是他們不會以與您的例子完全相同的方式書寫。 「核心」代碼往往是小功能,只能做一件事,然後組合在一起。例如「createRecord getThis <*> getThat <*> createThatThing」。我不知道你是否看過「monadster」談話(https://fsharpforfunandprofit.com/monadster/),但提出了一些這樣的想法。 – Grundoon

3

Scott(@Grundoon)的回答涵蓋了從OOP到FP的更直接的翻譯。如果您期望getThis,getThat函數中有一個不純,這是適當的。

一般來說,將函數作爲參數傳遞給其他函數是一件非常有用的事情(接收函數被稱爲高階函數),但它應該爲了實現可變性而完成。僅爲測試目的添加額外的函數參數導致David Heinemeier Hansson稱爲test-induced damage

在這個答案中,我想提供另一種觀點,儘管我想強調斯科特的回答與我自己的想法一致(並且我贊成)。它符合F#,因爲F#是一種混合語言,隱含的不純功能是可能的。

但是,在嚴格的功能語言(如Haskell)中,默認情況下功能爲pure。如果我們假設getThis,getThat等都是引用透明(純),函數調用可以用它們的返回值替換。

這意味着您不必替換它們與測試雙打

相反,你可以簡單地寫你的測試是這樣的:

[<Fact>] 
let testExample() = 
    // Create updateData and currentState values here... 

    let actual = applyUpdate updateData currentState 

    let expected = 
     { currentState with 
      This = getThis updateData currentState.Thingy 
      That = getThat updateData currentState.Thingy 
      // etc. } 
    expected =! actual // assert that expected equals actual 

你可以說,本次測試只複製了生產代碼,但這樣會使用OO風格測試雙打測試。我認爲真正的問題比OP更爲複雜,因爲OP似乎並不需要測試applyUpdate函數。

你也可以爭辯說,這個測試不是單元測試,我同意的語義;我稱這種測試爲Facade Tests

Pure functions are intrinsically testable,所以沒有理由改變他們的設計,使他們'可測試'。

+1

該函數確實具有非平凡的邏輯;我認爲我需要對依賴關係進行存根化,以便單獨測試這個邏輯。但是看看我的一些測試,我意識到我確實在幾種情況下應用了你的方法 - 我在寫它時確實感到尷尬,來自嚴格的OOP背景。感謝您闡明爲什麼它實際上可能是一種正確的方法! – Asik