В F# как передать коллекцию атрибуту InlineData xUnit

Я хотел бы использовать список, массив и/или seq в качестве параметра для InlineData xUnit.

В C# я могу сделать это:

using Xunit; //2.1.0

namespace CsTests
{
    public class Tests
    {
        [Theory]
        [InlineData(new[] {1, 2})]
        public void GivenCollectionItMustPassItToTest(int[] coll)
        {
            Assert.Equal(coll, coll);
        }
    }
}

В F# у меня есть это:

namespace XunitTests

module Tests =
  open Xunit //2.1.0

  [<Theory>]
  [<InlineData(8)>]
  [<InlineData(42)>]
  let ``given a value it must give it to the test`` (value : int) =
    Assert.Equal(value, value)

  [<Theory>]
  [<InlineData([1; 2])>]
  let ``given a list it should be able to pass it to the test``
  (coll : int list) =
    Assert.Equal<int list>(coll, coll)

  [<Theory>]
  [<InlineData([|3; 4|])>]
  let ``given an array it should be able to pass it to the test``
  (coll : int array) =
    Assert.Equal<int array>(coll, coll)

код F# дает следующие ошибки сборки:

файл library1.fs (13, 16): это недопустимое постоянное выражение или пользовательское значение атрибута

файл library1.fs (18, 16): это недопустимое выражение константы или пользовательский атрибут значение

ссылаясь на 2-ю и 3-ю тестовые теории.

можно ли использовать xUnit для передачи коллекций в атрибут InlineData?

3 ответов


InlineDataAttribute опирается на C# params механизм. Это то, что включает синтаксис по умолчанию InlineData в C#: -

[InlineData(1,2)]

ваша версия с построения массива:-

[InlineData( new object[] {1,2})]

это просто то, что complier переводит выше. В ту минуту, когда вы идете дальше, вы столкнетесь с теми же ограничениями на то, что CLI фактически включит - суть в том, что на уровне IL использование конструкторов атрибутов подразумевает, что все должно быть сведено к константы во время компиляции. Эквивалент F# вышеуказанного синтаксиса просто:[<InlineData(1,2)>], поэтому прямой ответ на ваш вопрос:

module UsingInlineData =
    [<Theory>]
    [<InlineData(1, 2)>]  
    [<InlineData(1, 1)>]  
    let v4 (a : int, b : int) : unit = Assert.NotEqual(a, b)

я не смог избежать риффинга на примере @bytebuster, хотя:) если мы определим помощника: -

type ClassDataBase(generator : obj [] seq) = 
    interface seq<obj []> with
        member this.GetEnumerator() = generator.GetEnumerator()
        member this.GetEnumerator() = 
            generator.GetEnumerator() :> System.Collections.IEnumerator

затем (если мы готовы отказаться от лени), мы можем злоупотреблять list чтобы избежать необходимости использовать seq / yield чтобы выиграть гольф-код:-

type MyArrays1() = 
    inherit ClassDataBase([ [| 3; 4 |]; [| 32; 42 |] ])

[<Theory>]
[<ClassData(typeof<MyArrays1>)>]
let v1 (a : int, b : int) : unit = Assert.NotEqual(a, b)

но сырой синтаксис seq можно сделать достаточно чисто, поэтому нет необходимости использовать его, как указано выше, вместо этого мы делаем:

let values : obj array seq = 
    seq { 
        yield [| 3; 4 |] 
        yield [| 32; 42 |] 
    }

type ValuesAsClassData() = 
    inherit ClassDataBase(values)

[<Theory; ClassData(typeof<ValuesAsClassData>)>]
let v2 (a : int, b : int) : unit = Assert.NotEqual(a, b)

однако, наиболее идиоматичным с xUnit v2 для меня является использование straight MemberData (что похоже на xUnit v1 PropertyData но обобщены также для работы на полях): -

[<Theory; MemberData("values")>]
let v3 (a : int, b : int) : unit = Assert.NotEqual(a, b)

ключевая вещь, чтобы получить право, чтобы положить : seq<obj> (или : obj array seq) на объявление последовательности или xUnit будет бросать на вас.


как описано в этот вопрос, вы можете использовать только литералы с InlineData. Списки не являются литералами.

однако, xUnit предоставляет ClassData который, кажется, делать то, что вам нужно.

этот вопрос обсуждает ту же проблему для C#.

чтобы использовать ClassData с тестами просто сделайте класс данных, реализующий seq<obj[]>:

type MyArrays () =    
    let values : seq<obj[]>  =
        seq {
            yield [|3; 4|]    // 1st test case
            yield [|32; 42|]  // 2nd test case, etc.
        }
    interface seq<obj[]> with
        member this.GetEnumerator () = values.GetEnumerator()
        member this.GetEnumerator () =
            values.GetEnumerator() :> System.Collections.IEnumerator

module Theories = 
    [<Theory>]
    [<ClassData(typeof<MyArrays1>)>]
    let ``given an array it should be able to pass it to the test`` (a : int, b : int) : unit = 
        Assert.NotEqual(a, b)

хотя это требует ручного кодирования, вы можете повторно использовать класс данных , что, по-видимому, полезно в реальных проектах, где мы часто проводим разные тесты с одними и теми же данными.


можно использовать FSharp.Reflection пространство имен для хорошего эффекта здесь. Рассмотрим некоторую гипотетическую функцию isAnswer : (string -> int -> bool) что вы хотите проверить с помощью нескольких примеров.

вот так:

open FSharp.Reflection
open Xunit

type TestData() =
  static member MyTestData =
    [ ("smallest prime?", 2, true)
      ("how many roads must a man walk down?", 41, false) 
    ] |> Seq.map FSharpValue.GetTupleFields

[<Theory; MemberData("MyTestData", MemberType=typeof<TestData>)>]
let myTest (q, a, expected) =
  Assert.Equals(isAnswer q a, expected)

ключевая вещь |> Seq.map FSharpValue.GetTupleFields линии. Он берет список кортежей (вы должны использовать кортежи, чтобы разрешить различные типы аргументов) и преобразует его в IEnumerable<obj[]> этого ожидает XUnit.