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

如何使NUnit运行未由模块导出的F#测试,f#,nunit,F#,Nunit,我用F#编写了一个大模块,它碰巧有一个简单的接口。该模块包含大约1000行代码、50个单元测试,只输出一个易于理解的函数 接下来要做的自然事情是编写一个小的fsi文件。这有许多好处,包括防止名称空间污染,为文档提供一个明显的位置,确保如果有人决定重用内部构件,他们将有动力将其清除,毫无疑问还有许多其他好处。我确信我在这里向唱诗班布道,但仍然觉得有必要解释为什么我觉得有fsi文件是有帮助的 现在问题来了。NUnit将不再运行单元测试,顽固地声称它们不是公开的。那是因为它们在任何方面都不是接口的一部

我用F#编写了一个大模块,它碰巧有一个简单的接口。该模块包含大约1000行代码、50个单元测试,只输出一个易于理解的函数

接下来要做的自然事情是编写一个小的fsi文件。这有许多好处,包括防止名称空间污染,为文档提供一个明显的位置,确保如果有人决定重用内部构件,他们将有动力将其清除,毫无疑问还有许多其他好处。我确信我在这里向唱诗班布道,但仍然觉得有必要解释为什么我觉得有fsi文件是有帮助的

现在问题来了。NUnit将不再运行单元测试,顽固地声称它们不是公开的。那是因为它们在任何方面都不是接口的一部分。尽管如此,我并不特别想将它们添加到接口中,因为这意味着每次我添加另一个测试时都要对其进行更新,而且还会使fsi文件膨胀一个数量级

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


编辑:非常感谢所有回应的人。我对两个答案都投了赞成票。我本来想分一份赏金,但由于这似乎不可能,我会(有点武断地)接受托马斯的回答

如果要添加一个
fsi
文件来指定源代码中模块和函数的可见性,则需要包含所有应该公开访问的函数的声明。这意味着,如果NUnit要求测试是公共函数,则需要将它们包含在
fsi
文件中

但是,还有另一种方法可以指定F#中的可见性,您可以只在声明中添加适当的可见性修饰符,而不是使用
fsi
file。通过这种方式,您可以隐藏所有实现细节,并仅导出主功能和测试:

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) 
名称空间MyLibrary
打开NUnit.Framework
//实现详细信息可在此模块中找到
//(在库外不可见)
模块专用内部=
设foo n=n*2
设杆n=n+1
//公共模块可以包含公共API(并使用内部实现)
模块公共MyModule=
开放式内部
让doWork n=foo(bar n)
//为了使测试对NUnit可见,可以将这些测试放在公共模块中
//(但他们仍然可以从“内部”访问所有函数)
模块公共测试=
打开MyModule
[]
让``对n=1``()=
Assert.Equals(doWork 1,4)
与使用
fsi
文件相比,这有一个缺点,即您没有一个单独的文件来很好地描述API的重要部分。但是,您将得到所需的—隐藏实现细节,只公开单个函数和测试。

您可以求助于使用反射来调用您的私有测试方法:您将拥有一个公共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

你试过使用InternalsVisibleTo吗?我觉得我的问题有点不同。我已经可以很好地编译测试了。但是,如果不从模块导出它们,则无法运行它们。我想,如果我能让它说NUnit可以访问所有内部构件,那么该属性会有所帮助。但我不知道从哪里开始,谷歌似乎也不知道。显然,我可以在将测试移动到另一个文件后使用该属性,但这是一个更糟糕的问题。我将它们用作可执行文档,因此它们确实需要接近代码。非常感谢您的回复。如果它编译了,它应该会运行。除非测试与实际代码在同一个程序集中,或者类似的地方。测试与被测试的代码在同一个文件和程序集中。我知道有很多“建议”是用英语说的,不应该这样做。但是没有任何理由,我的观点是,这显然是错误的。当使用这种方法时,可能不可能交错测试和代码?@user1002059这也是可能的,但您必须使用
let private foo()=…
或使用
let public test_for_foo()=…
标记每个函数,并公开包含一些公共函数的所有模块。这意味着更多的注释,但这是可能的。非常感谢。这是一个我没有想到的有趣的解决方案。然而,我想知道它在重新实现NUnit的方向上是否走得太远了。例如,如果任何测试预期出现异常,使用[]之类的东西,甚至安装和拆卸,那么将有大量工作要做?再次感谢。不客气:)为了在Silverlight下运行我的xUnit.net/Unquote测试,我最初提出了类似于此解决方案的方法:。我可以想象,完全支持NUnit能够提出的所有类型的断言会增加这里的复杂性(您确实提供了一种NUnit测试运行程序的重新实现),但这对我来说不是问题,因为Unquote只使用
NUnit.Framework.AssertionException
,并在此基础上提供了自己的断言API。由于NUnit是开源的,另一个选择是为自己构建一个自定义版本,它删除了NUnit跳过私有测试方法的逻辑(它确实看到了它们,并且可以运行它们,但出于任何愚蠢的原因,它故意选择不运行它们。)
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)
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).