Могу ли я вызывать вложенные функции для модульного тестирования?

В F # я хочу выполнить модульное тестирование функции с несколькими уровнями вложенных функций. Я хочу также иметь возможность тестировать вложенные функции по отдельности, но я не знаю, как их вызывать. При отладке каждая из этих вложенных функций вызывается как тип функционального объекта, но я не знаю, могу ли я получить к ним доступ во время компиляции.

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

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

Очень маленький пример:

let range a b =
    let lower =  ceil a |> int
    let upper =  floor b |> int
    if lower > upper then
        Seq.empty
    else
        seq{ for i in lower..upper -> i}

Как я могу проверить правильность работы lower или upper без изменения вложенности кода?


person mattgately    schedule 31.05.2012    source источник
comment
+1 Невозможно, но мне любопытно увидеть предлагаемые обходные пути. :-]   -  person ildjarn    schedule 01.06.2012
comment
Если range работает правильно, не могли бы вы предположить, что lower и upper тоже работают?   -  person Daniel    schedule 01.06.2012
comment
@ Даниэль, это простой пример. И на самом деле, не очень хороший, так как нижняя и верхняя не связаны с функциями. Но как насчет гораздо более сложного примера, где вложенные вспомогательные функции могут быть довольно сложными сами по себе, и поэтому их следует тестировать отдельно от всей содержащей их функции.   -  person mattgately    schedule 01.06.2012
comment
@mattgately Вы можете пересмотреть определение модуля в термине модульный тест. Если рассматриваемый блок является функцией, то вы тестируете не внутреннюю функциональность — вы тестируете функцию. Я имею в виду, если вам нужно протестировать нижний и верхний (или какой-то их аналог), сделайте их своей собственной автономной единицей кода.   -  person Onorio Catenacci    schedule 01.06.2012
comment
На самом деле это отдельные юниты: ceil и floor. Они являются частью стандартной библиотеки. Не нужно тестировать его на своей стороне.   -  person Aleš Roubíček    schedule 05.06.2012


Ответы (1)


Я согласен с комментарием Дэниэлса - если внешняя функция работает правильно, вам не нужно тестировать какие-либо внутренние функции. Внутренние функции на самом деле являются деталями реализации, которые не должны иметь значения (особенно в функциональном коде, где вывод не зависит ни от чего, кроме ввода). В C# вы также не проверяете, правильно ли работает цикл for или цикл while внутри вашего метода.

Если и внутренняя, и внешняя функции слишком сложны, то, возможно, было бы лучше написать внутреннюю функцию как отдельную функцию.

Тем не менее, вы можете, конечно, возиться с скомпилированной сборкой, используя отражение и вызывать внутреннюю функцию. Внутренние функции компилируются как классы с конструктором, принимающим замыкание (захваченные значения внешней функции), и методом Invoke, принимающим фактические параметры.

Следующий тривиальный пример работает, хотя я не проверял его на чем-то более реалистичном:

open NUnit.Framework

// Function with 'inner' that captures the argument 'a' and takes additional 'x'    
let outer a b = 
  let inner x = x + a + 1
  (inner a) * (inner b)

// Unit tests that use reflection in a hacky way to test 'inner'
[<TestFixture>]
module Tests = 
  open System
  open System.Reflection

  // Runs the specified compiled function - assumes that 'name' of inner functions
  // is unique in the current assembly (!) and that you can correctly guess what 
  // are the variables captured by the closure (!)
  let run name closure args = 
    // Lots of unchecked assumptions all the way through...
    let typ =
      Assembly.GetExecutingAssembly().GetTypes()  
      |> Seq.find (fun typ -> 
          let at = typ.Name.IndexOf('@')
          (at > 0) && (typ.Name.Substring(0, at) = name) )
    let flags = BindingFlags.Instance ||| BindingFlags.NonPublic
    let ctor = typ.GetConstructors(flags) |> Seq.head
    let f = ctor.Invoke(closure)
    let invoke = f.GetType().GetMethod("Invoke")
    invoke.Invoke(f, args)

  /// Test that 'inner 10' returns '14' if inside outer where 'a = 3'
  [<Test>]
  let test () = 
    Assert.AreEqual(run "inner" [| box 3 |] [| box 10 |], 14)
person Tomas Petricek    schedule 31.05.2012
comment
Спасибо @Томас. Мне было интересно, как это можно сделать с отражением. Как я и ожидал, это очень сложно и не стоит делать из-за очень недружественного синтаксиса для выполнения простых внутренних функций. Я надеялся, что будет более простой способ предоставления аргументов замыкания, и что тестирование таким образом будет возможно без изменения структуры программы, что наилучшим образом иллюстрирует иерархию проблемы. - person mattgately; 01.06.2012
comment
+1 для внутренних функций - это действительно деталь реализации, которая не должна иметь значения (особенно в функциональном коде, где вывод не зависит ни от чего, кроме ввода) (выделено мной). - person Stephen Swensen; 01.06.2012
comment
@mattgately Приложив немного усилий, вы могли бы сделать синтаксис более приятным. Используя оператор ?, вы, вероятно, могли бы получить что-то вроде Assert.AreEqual(funcs?inner 3 10, 14) или, если бы у вас была функция, которая принимает два аргумента закрытия и два фактических аргумента, то что-то вроде funcs?inner (3.14, "pi") (1, "hi"). Но я думаю, что в любом случае не нужно тестировать внутренние функции. - person Tomas Petricek; 01.06.2012
comment
@mattgately Связанный пример, в котором ? используется аналогичным образом, см. в этой статье: msdn. microsoft.com/en-us/library/hh304373 - person Tomas Petricek; 01.06.2012
comment
Я принял ваш ответ, поскольку объяснение и комментарии, по крайней мере, дают мне достаточно оснований отказаться от того, что я называл модульным тестированием вложенных функций. - person mattgately; 03.06.2012
comment
Мне все еще было бы интересно, если бы у кого-нибудь был способ сделать это, даже если это не строго модульное тестирование. Причина в том, что я полагаю, что функциональная парадигма позволяет мне реализовать программу таким образом, который наиболее точно отражает реальную проблему. Кроме того, я считаю, что если функция действительно используется только в одной функции (и имеет общие входные данные), то существует естественная иерархия, которую следует использовать. Я бы предпочел не изменять естественную структуру, чтобы облегчить тестирование. Возможно, у кого-то есть предложение, как вписать тесты в естественную иерархию вложенности? - person mattgately; 03.06.2012