В 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})]

это просто то, что компилятор переводит выше. В ту минуту, когда вы идете дальше, вы столкнетесь с теми же ограничениями на то, что на самом деле будет включать 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 для меня является использование прямой MemberData (что походит на xUnit v1's 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)

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

Вы также можете использовать данные члена без класса:

let memberDataProperty:=
seq {
    yield [|"param1":> Object; param2 :> Object; expectedResult :> Object |]
}

[<Theory>]
[<MemberData("memberDataProperty")>]
let ``Can use MemberData`` param1 param2 expectedResult = ...

Вы можете использовать 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 ожидает.

Одна из возможностей заключается в использовании xUnit's MemberData приписывать. Недостатком этого подхода является то, что этот параметризованный тест отображается в обозревателе тестов Visual Studio как один тест вместо двух отдельных тестов, поскольку в коллекциях отсутствует xUnit. IXunitSerializable interface и xUnit не добавили встроенную поддержку сериализации для этого типа. См. Xunit / xunit / Issues / 429 для получения дополнительной информации.

Вот минимальный рабочий пример.

module TestModule

  open Xunit

  type TestType () =
    static member TestProperty
      with get() : obj[] list =
        [
          [| [0]; "a" |]
          [| [1;2]; "b" |]
        ]

    [<Theory>]
    [<MemberData("TestProperty")>]            
    member __.TestMethod (a:int list) (b:string) =
      Assert.Equal(1, a.Length)

Смотрите также этот похожий вопрос, в котором я даю аналогичный ответ.

Опираясь на блестящий ответ @Assassin - теперь у нас есть неявные выходы, вы можете поместить тестовые примеры в массив и обойтись без yieldс. У меня также возникнет соблазн добавить нахальный частный оператор для обработки преобразований объектов. Таким образом:

open System
open Xunit

let inline private (~~) x = x :> Object

let degreesToRadiansCases =
    [|
        // Degrees; Radians
        [|   ~~0.0;            ~~0.0  |]
        [| ~~360.0; ~~(Math.PI * 2.0) |]
    |]

[<Theory>]
[<MemberData("degreesToRadiansCases")>]
let ``Convert from degrees to radians`` (degrees, radians) =
    let expected = radians
    let actual = Geodesy.Angle.toRadians degrees
    Assert.Equal(expected, actual)

let stringCases =
    [|
        [| ~~99; ~~"hello1" |] 
        [| ~~99; ~~"hello2" |] 
    |]

[<Theory>]
[<MemberData("stringCases")>]
let ``tests`` (i, s) =
    printfn "%i %s" i s
    Assert.Equal(s, "hello1")
Другие вопросы по тегам