如何使NUnit运行未由模块导出的F#测试
我用F#编写了一个大模块,它碰巧有一个简单的接口。该模块包含大约1000行代码、50个单元测试,只输出一个易于理解的函数 接下来要做的自然事情是编写一个小的fsi文件。这有许多好处,包括防止名称空间污染,为文档提供一个明显的位置,确保如果有人决定重用内部构件,他们将有动力将其清除,毫无疑问还有许多其他好处。我确信我在这里向唱诗班布道,但仍然觉得有必要解释为什么我觉得有fsi文件是有帮助的 现在问题来了。NUnit将不再运行单元测试,顽固地声称它们不是公开的。那是因为它们在任何方面都不是接口的一部分。尽管如此,我并不特别想将它们添加到接口中,因为这意味着每次我添加另一个测试时都要对其进行更新,而且还会使fsi文件膨胀一个数量级 我认为一个简单的解决方法是将代码移到其他地方,将其导入一个很小的.fs文件,然后只转发一个函数。运气好的话,每个人都会同意这简直是令人反感。请问有更好的办法吗如何使NUnit运行未由模块导出的F#测试,f#,nunit,F#,Nunit,我用F#编写了一个大模块,它碰巧有一个简单的接口。该模块包含大约1000行代码、50个单元测试,只输出一个易于理解的函数 接下来要做的自然事情是编写一个小的fsi文件。这有许多好处,包括防止名称空间污染,为文档提供一个明显的位置,确保如果有人决定重用内部构件,他们将有动力将其清除,毫无疑问还有许多其他好处。我确信我在这里向唱诗班布道,但仍然觉得有必要解释为什么我觉得有fsi文件是有帮助的 现在问题来了。NUnit将不再运行单元测试,顽固地声称它们不是公开的。那是因为它们在任何方面都不是接口的一部
编辑:非常感谢所有回应的人。我对两个答案都投了赞成票。我本来想分一份赏金,但由于这似乎不可能,我会(有点武断地)接受托马斯的回答 如果要添加一个
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).