2011-12-04 12 views
7

He escrito un módulo grande en F # que tiene una interfaz trivial. El módulo contiene aproximadamente 1000 líneas de código, 50 pruebas unitarias y exporta una sola función fácil de entender.Cómo hago que NUnit ejecute las pruebas F # no exportadas por un módulo

Lo más natural es escribir un pequeño archivo fsi. Esto tiene numerosas ventajas, incluida la prevención de la contaminación del espacio de nombres, proporcionando un lugar obvio para la documentación, asegurándose de que si alguien decide volver a utilizar las partes internas, tendrá un incentivo para eliminarlas, y sin duda muchas otras. Estoy seguro de que estoy predicando al coro aquí, pero todavía siento que vale la pena explicar por qué creo que es útil tener el archivo fsi.

Ahora el problema. NUnit ya no ejecutará las pruebas unitarias y alegará recalcitrantemente que no son públicas. Bueno, eso sería porque no son de ninguna manera una parte de la interfaz. No quiero añadirlos particularmente a la interfaz a pesar de eso, ya que significaría actualizarlo cada vez que añadí otra prueba, y también que inflaría el archivo fsi en un orden de magnitud.

Supongo que una solución trivial es mover el código a otro lugar, importarlo en un pequeño archivo .fs y simplemente reenviar la función. Con un poco de suerte, todos estarán de acuerdo en que es simplemente repugnante. ¿Hay una mejor manera, por favor?

Editar: muchas gracias a todos los que respondieron. Subí ambas respuestas. Me hubiera gustado dividir la recompensa, sin embargo, como eso no parece posible, aceptaré (de forma algo arbitraria) la respuesta de Tomás.

+1

¿Ha intentado utilizar InternalsVisibleTo? http://devlicio.us/blogs/derik_whittaker/archive/2007/04/09/internalsvisibleto-testing-internal-methods-in-net-2-0.aspx –

+0

Creo que mi problema es un poco diferente. Ya puedo compilar las pruebas muy bien. Sin embargo, no puedo ejecutarlos si no los exporto desde el módulo. Supongo que el atributo me ayudaría si pudiera decir que NUnit tiene acceso a todos los internos. Pero no tengo idea de dónde comenzar con eso, y tampoco, parece, lo hace Google. Obviamente podría usar el atributo después de mover las pruebas a un archivo diferente, pero ese es un mal peor. Los uso como documentación ejecutable, por lo que realmente deben estar cerca del código. Muchas gracias por la respuesta. – user1002059

+0

Si se compila, debería ejecutarse. A menos que tenga las pruebas en el mismo ensamblaje que el código real, o algo así. –

Respuesta

6

Si está agregando un archivo fsi para especificar la visibilidad de los módulos y funciones en su fuente, deberá incluir declaraciones de todas las funciones a las que se debe tener acceso público. Esto significa que si NUnit requiere que las pruebas sean funciones públicas, deberá incluirlas en el archivo fsi.

Sin embargo, también hay otra forma de especificar la visibilidad en F # - en lugar de usar el archivo fsi, puede agregar modificadores de visibilidad adecuados a sus declaraciones. De esta manera, se puede ocultar todos los detalles de implementación y la exportación solamente la función principal y exámenes:

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) 

En comparación con el uso de fsi archivos, esto tiene el inconveniente de que no tiene un archivo independiente que muy bien describe solamente la partes importantes de tu API. Sin embargo, obtendrá lo que necesita: oculte los detalles de la implementación y exponga solo una función y las pruebas.

+0

Muchas gracias por la respuesta. ¿No es posible, supuestamente, intercalar las pruebas y el código cuando se utiliza este enfoque? – user1002059

+0

@ user1002059 Eso sería posible también, pero tendría que marcar cada función usando 'let private foo() = ...' o usando 'let public test_for_foo() = ...' y hacer públicos todos los módulos que contienen algunas funciones públicas. Eso significa más anotaciones, pero es una posibilidad. –

2

Enfoque

Se podría recurrir al uso de la reflexión para invocar sus métodos de prueba privados: usted tendría un único método público de prueba NUnit los bucles que sobre todos los métodos privados en el montaje de la invocación de los que tienen el atributo de prueba. El gran inconveniente de este enfoque es que solo puedes ver un método de prueba fallido a la vez (pero tal vez podrías buscar algo creativo como usar pruebas parametrizadas para solucionarlo).

Ejemplo

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 (aplicación de nuestra "estrategia")

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) 

Ejemplo de salida (usando TestDriven.NET)

Aviso nunca llegamos a testImplMethod4 puesto que falla en 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

Muchas gracias. Esa es una solución interesante en la que no he pensado. Sin embargo, me pregunto si está yendo demasiado lejos en la dirección de volver a implementar NUnit. Por ejemplo, si alguna de las pruebas esperaba una excepción, usaba cosas como [] o incluso configuración y desmontaje, ¿habría mucho trabajo por hacer? Gracias de nuevo. – user1002059

+0

De nada :) Originalmente se me ocurrió algo similar a esta solución para ejecutar mis pruebas xUnit.net/Unquote en Silverlight: http://stackoverflow.com/questions/7053245/how-to-run-f -silverlight-library-project-holding-xunit-net-contrib-based-unit-t Me imagino que respaldar completamente todos los tipos de aserciones que NUnit puede plantear alguna vez ampliaría la complejidad aquí (de hecho está proporcionando una especie de reimplementación de corredores de prueba NUnit), pero eso no fue un problema para mí, ya que Unquote solo usa 'NUnit. Framework.AssertionException' y proporciona su propia API de aserción sobre eso. –

+0

Otra opción desde que NUnit es de código abierto es hackear una compilación personalizada que elimina la lógica de NUnit para omitir los métodos de prueba privados (los ve y puede ejecutarlos, pero por alguna razón tonta elige deliberadamente no admitirlos) - o , como sugerí en mi comentario sobre tu pregunta, pasa a xUnit.net ya que no tiene escrúpulos para ejecutar métodos de prueba privados, y la conversión debería ser bastante sencilla (por supuesto, puede que tengas algún requisito que te vincule a NUnit) . –

Cuestiones relacionadas