如何使NUnit运行未由模块导出的F#测试

时间:2011-12-04 11:01:08

标签: f# nunit

我在F#中编写了一个大型模块,碰巧有一个简单的界面。该模块包含大约1000行代码,50个单元测试,并只导出一个易于理解的功能。

接下来要做的就是写一个小的fsi文件。这有许多优点,包括防止命名空间污染,为文档提供一个明显的位置,确保如果有人决定重用内部,他们将有动力干净地将它们分解出来,毫无疑问,许多其他人。我相信我在这里向合唱团讲道,但仍然觉得值得解释为什么我认为有fsi文件是有帮助的。

现在问题。 NUnit将不再运行单元测试,谴责声称它们不公开。那么,那将是因为它们不以任何方式成为界面的一部分。我并不特别希望将它们添加到界面中,尽管如此,因为它意味着每次添加另一个测试时都会更新它,并且它会使fsi文件膨胀一个数量级。

我认为一个简单的解决方法是将代码移动到其他地方,将其导入到一个很小的.fs文件中,然后转发一个函数。幸运的是,每个人都会认为这只是令人反感。请问有更好的方法吗?

编辑:非常感谢所有回复的人。我赞同这两个答案。我本来想分开赏金,但是因为这似乎不可能(我有点随意)接受Tomas的回答。

2 个答案:

答案 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的重要部分。但是,您将获得所需内容 - 隐藏实现细节并仅显示单个函数和测试。

答案 1 :(得分:2)

方法

您可以使用反射来调用您的私有测试方法:您将拥有一个公共NUnit测试方法,该方法循环调用程序集中的所有私有方法,并调用具有Test属性的方法。这种方法的一个重要缺点是,你一次只能看到一种失败的测试方法(但也许你可以研究一些创造性的东西,比如使用参数化测试来解决这个问题)。

实施例

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).