2011-12-04 102 views
7

我已經在F#中編寫了一個大型模塊,它恰好有一個簡單的接口。該模塊包含約1000行代碼,50個單元測試,並且只導出一個易於理解的功能。如何讓NUnit運行F#測試不是由模塊導出

接下來要做的事情就是寫一個小小的fsi文件。這具有許多優點,包括防止命名空間污染,爲文檔提供一個明顯的位置,確保如果任何人決定重用內部組件,他們將有動機將其乾淨地分解出來,毫無疑問還有其他許多優點。我確信我在這裏向合唱團傳道,但仍然覺得值得解釋爲什麼我覺得有fsi文件是有幫助的。

現在的問題。 NUnit將不再執行單元測試,並且不願意公開聲稱它們不公開。那麼,那將是因爲他們不以任何方式成爲界面的一部分。儘管如此,我並不是特別想將它們添加到接口中,因爲這意味着每次添加另一個測試時都會更新它,並且它會使fsi文件膨脹一個數量級。

我想一個簡單的解決方法是將代碼移動到其他地方,將其導入到一個微小的.fs文件,並轉發一個函數。幸運的是,每個人都會同意這只是一場反抗。請問有更好的方法嗎?

編輯:非常感謝大家的迴應。我提出了兩個答案。我本想喜歡分割賞金,但是因爲這看起來不太可能,我會(有些任意地)接受託馬斯的答案。

+1

您是否嘗試過使用InternalsVisibleTo? http://devlicio.us/blogs/derik_whittaker/archive/2007/04/09/internalsvisibleto-testing-internal-methods-in-net-2-0.aspx –

+0

我認爲我的問題有點不同。我已經可以編譯測試了。但是,如果我不從模塊中導出它們,我無法運行它們。如果我能說出所有的內部函數都可以被NUnit訪問,我想這個屬性會有所幫助。但我不知道該從哪開始,也不知道 - 谷歌。將測試移動到不同的文件後,我顯然可以使用該屬性,但這是一種更糟糕的惡果。我將它們用作可執行文檔,因此它們確實需要接近代碼。非常感謝您的回覆。 – user1002059

+0

如果編譯,它應該運行。除非你碰巧在與實際代碼相同的程序集中進行測試,或者類似的東西。 –

回答

6

如果您要添加一個fsi文件來指定源代碼中模塊和函數的可見性,那麼您需要包含應公開訪問的所有函數的聲明。這意味着如果NUnit要求測試是公共職能,則需要將它們包含在fsi文件中。

但是,還有另一種指定F#中的可見性的方法 - 而不是使用fsi文件,您可以在聲明中添加適當的可見性修飾符。通過這種方式,可以隱藏所有的實施細則,並只導出主要功能和測試:

namespace MyLibrary 
open NUnit.Framework 

// Implementation details can be in this module 
// (which will not be visible outside of the library) 
module private Internal = 
    let foo n = n * 2 
    let bar n = n + 1 

// A public module can contain the public API (and use internal implementation)  
module public MyModule = 
    open Internal 
    let doWork n = foo (bar n) 

// To make the tests visible to NUnit, these can be placed in a public module 
// (but they can still access all functions from 'Internal') 
module public Tests = 
    open MyModule 

    [<Test>] 
    let ``does work for n = 1``() = 
    Assert.Equals(doWork 1, 4) 

使用fsi文件相比,該有你沒有隻說好聽描述了一個單獨的文件的缺點你的API的重要組成部分。然而,你會得到你所需要的 - 隱藏實現細節,只公開一個函數和測試。

+0

非常感謝爲答案。當使用這種方法時,估計不可能交叉測試和代碼? – user1002059

+0

@ user1002059這也是可能的,但你必須使用'let private foo()= ...'或者使用'let public test_for_foo()= ...'來標記每一個函數,並且公開所有包含一些公共職能。這意味着更多的註釋,但這是一種可能性。 –

2

方法

你可以訴諸使用反射來調用您的私人測試方法:你有哪些遍歷所有的私有方法中單一的公共NUnit測試方法組裝調用那些測試屬性。這種方法最大的缺點是你一次只能看到一種失敗的測試方法(但也許你可以看看像使用參數化測試來解決這個問題的創意)。

Program.fsi

namespace MyNs 

module Program = 
    val visibleMethod: int -> int 

Program.fs

namespace MyNs 

open NUnit.Framework 

module Program = 
    let implMethod1 x y = 
     x + y 

    [<Test>] 
    let testImpleMethod1() = 
     Assert.AreEqual(implMethod1 1 1, 2) 

    let implMethod2 x y z = 
     x + y + z 

    [<Test>] 
    let testImpleMethod2() = 
     Assert.AreEqual(implMethod2 1 1 1, 3) 

    let implMethod3 x y z r = 
     x + y + z + r 

    [<Test>] 
    let testImpleMethod3() = 
     Assert.AreEqual(implMethod3 1 1 1 1, -1) 

    let implMethod4 x y z r s = 
     x + y + z + r + s 

    [<Test>] 
    let testImpleMethod4() = 
     Assert.AreEqual(implMethod4 1 1 1 1 1, 5) 

    let visibleMethod x = 
     implMethod1 x x 
     + implMethod2 x x x 
     + implMethod3 x x x x 

TestProxy。FS(實施我們的 「辦法」)

module TestProxy 

open NUnit.Framework 

[<Test>] 
let run() = 
    ///we only want static (i.e. let bound functions of a module), 
    ///non-public methods (exclude any public methods, including this method, 
    ///since those will not be skipped by nunit) 
    let bindingFlags = System.Reflection.BindingFlags.Static ||| System.Reflection.BindingFlags.NonPublic 

    ///returns true if the given obj is of type TestAttribute, the attribute used for marking nunit test methods 
    let isTestAttr (attr:obj) = 
     match attr with 
     | :? NUnit.Framework.TestAttribute -> true 
     | _ -> false 

    let assm = System.Reflection.Assembly.GetExecutingAssembly() 
    let tys = assm.GetTypes() 
    let mutable count = 0 
    for ty in tys do 
     let methods = ty.GetMethods(bindingFlags) 
     for mi in methods do 
      let attrs = mi.GetCustomAttributes(false) 
      if attrs |> Array.exists isTestAttr then 
       //using stdout w/ flush instead of printf to ensure messages printed to screen in sequence 
       stdout.Write(sprintf "running test `%s`..." mi.Name) 
       stdout.Flush() 
       mi.Invoke(null,null) |> ignore 
       stdout.WriteLine("passed") 
       count <- count + 1 
    stdout.WriteLine(sprintf "All %i tests passed." count) 

示例輸出(使用TestDriven.NET)

通知我們永遠不會testImplMethod4,因爲它沒有在testImpleMethod3:

running test `testImpleMethod1`...passed 
running test `testImpleMethod2`...passed 
running test `testImpleMethod3`...Test 'TestProxy.run' failed: System.Reflection.TargetInvocationException : Exception has been thrown by the target of an invocation. 
    ----> NUnit.Framework.AssertionException : Expected: 4 
    But was: -1 
    at System.RuntimeMethodHandle._InvokeMethodFast(IRuntimeMethodInfo method, Object target, Object[] arguments, SignatureStruct& sig, MethodAttributes methodAttributes, RuntimeType typeOwner) 
    at System.RuntimeMethodHandle.InvokeMethodFast(IRuntimeMethodInfo method, Object target, Object[] arguments, Signature sig, MethodAttributes methodAttributes, RuntimeType typeOwner) 
    at System.Reflection.RuntimeMethodInfo.Invoke(Object obj, BindingFlags invokeAttr, Binder binder, Object[] parameters, CultureInfo culture, Boolean skipVisibilityChecks) 
    at System.Reflection.RuntimeMethodInfo.Invoke(Object obj, BindingFlags invokeAttr, Binder binder, Object[] parameters, CultureInfo culture) 
    at System.Reflection.MethodBase.Invoke(Object obj, Object[] parameters) 
    C:\Users\Stephen\Documents\Visual Studio 2010\Projects\FsOverflow\FsOverflow\TestProxy.fs(29,0): at TestProxy.run() 
    --AssertionException 
    C:\Users\Stephen\Documents\Visual Studio 2010\Projects\FsOverflow\FsOverflow\Program.fs(25,0): at MyNs.Program.testImpleMethod3() 

0 passed, 1 failed, 4 skipped (see 'Task List'), took 0.41 seconds (NUnit 2.5.10). 
+0

非常感謝。這是一個我沒有想到的有趣的解決方案。但是,我想知道在重新實現NUnit方面是否會有點過頭。例如,如果任何測試期望有一個例外,使用了諸如[]之類的東西,甚至是設置和拆卸,那麼將會有很多工作要做?再次感謝。 – user1002059

+0

不客氣:)我最初想出了一些與此解決方案類似的東西,以便在Silverlight下運行我的xUnit.net/Unquote測試:http://stackoverflow.com/questions/7053245/how-to-run-f -silverlight庫項目保持-的xUnit淨的contrib基於單元-T。我可以想象,完全支持NUnit所能提出的所有類型的斷言將會增加這裏的複雜性(您確實提供了一種NUnit測試運行器重新實現),但這對我來說不是問題,因爲Unquote只使用NUnit。 Framework.AssertionException'並提供它自己的斷言API。 –

+0

自NUnit開源以來的另一種選擇是爲自己創建一個自定義構建,它刪除NUnit的邏輯以跳過私有測試方法(它確實看到它們,並且可以運行它們,但是因爲任何愚蠢的理由故意選擇不支持它們),或者,正如我在你對我的問題的評論中所建議的那樣,切換到xUnit.net,因爲它對運行私有測試方法沒有顧忌,並且轉換應該非常輕鬆(當然,你可能有一些要求將你綁定到NUnit) 。 –