Как заставить NUnit запускать тесты F #, не экспортируемые модулем - PullRequest
7 голосов
/ 04 декабря 2011

Я написал большой модуль на F #, который имеет тривиальный интерфейс.Модуль содержит около 1000 строк кода, 50 модульных тестов и экспортирует только одну понятную функцию.

Естественно, что делать дальше - написать крошечный файл с расширением fsi.Это имеет многочисленные преимущества, в том числе предотвращение загрязнения пространства имен, обеспечение очевидного места для документации, обеспечение того, чтобы, если кто-либо решит повторно использовать внутренние компоненты, у них будет стимул для их четкого разделения, и, несомненно, многие другие.Я уверен, что я проповедую хору здесь, но все же чувствую, что стоит объяснить, почему я считаю полезным иметь файл fsi.

Теперь проблема.NUnit не будет работать модульных тестов больше, recalcitrantly утверждая, что они не являются общедоступными.Ну, это потому, что они никоим образом не являются частью интерфейса.Я не особенно хочу добавлять их в интерфейс, несмотря на это, поскольку это будет означать обновление его каждый раз, когда я добавляю другой тест, а также то, что он будет раздувать файл fsi на порядок.

Я полагаю, что тривиальный обходной путь - это переместить код в другое место, импортировать его в крошечный файл .fs и просто переслать одну функцию.Если повезет, все согласятся, что это просто отвратительно.Есть ли лучший способ, пожалуйста?

Редактировать: большое спасибо всем, кто откликнулся.Я проголосовал за оба ответа.Я хотел бы разделить щедрость, однако, поскольку это кажется невозможным, я (несколько произвольно) приму ответ Томаса.

Ответы [ 2 ]

6 голосов
/ 06 декабря 2011

Если вы добавляете файл 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.Однако вы получите то, что вам нужно - скрыть детали реализации и выставить только одну функцию и тесты.

2 голосов
/ 08 декабря 2011

Approach

Вы можете прибегнуть к использованию отражения для вызова ваших частных тестовых методов: у вас будет один общедоступный тестовый метод 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).
...