Przekaż złożone parametry do [Teoria]

100

Xunit ma fajną funkcję : możesz utworzyć jeden test z Theoryatrybutem i umieścić dane w InlineDataatrybutach, a xUnit wygeneruje wiele testów i przetestuje je wszystkie.

Chcę mieć coś takiego, ale parametry do mojej metody nie są „proste” (dane jak string, int, double), ale lista mojej klasy:

public static void WriteReportsToMemoryStream(
    IEnumerable<MyCustomClass> listReport,
    MemoryStream ms,
    StreamWriter writer) { ... }
zchpit
źródło
1
Kompletny przewodnik, który wysyła złożone obiekty jako parametr do metod testowania złożonych typów w teście jednostkowym
Iman Bahrampour

Odpowiedzi:

139

W xxxxDataXUnit jest wiele atrybutów. Sprawdź na przykład PropertyDataatrybut.

Możesz zaimplementować właściwość, która zwraca IEnumerable<object[]>. Każda object[]wygenerowana przez tę metodę zostanie następnie „rozpakowana” jako parametry dla pojedynczego wywołania [Theory]metody.

Inną opcją jest ClassData, która działa tak samo, ale umożliwia łatwe współdzielenie „generatorów” między testami w różnych klasach / przestrzeniach nazw, a także oddziela „generatory danych” od rzeczywistych metod testowych.

Zobacz np. Te przykłady stąd :

Przykład PropertyData

public class StringTests2
{
    [Theory, PropertyData(nameof(SplitCountData))]
    public void SplitCount(string input, int expectedCount)
    {
        var actualCount = input.Split(' ').Count();
        Assert.Equal(expectedCount, actualCount);
    }

    public static IEnumerable<object[]> SplitCountData
    {
        get
        {
            // Or this could read from a file. :)
            return new[]
            {
                new object[] { "xUnit", 1 },
                new object[] { "is fun", 2 },
                new object[] { "to test with", 3 }
            };
        }
    }
}

Przykład ClassData

public class StringTests3
{
    [Theory, ClassData(typeof(IndexOfData))]
    public void IndexOf(string input, char letter, int expected)
    {
        var actual = input.IndexOf(letter);
        Assert.Equal(expected, actual);
    }
}

public class IndexOfData : IEnumerable<object[]>
{
    private readonly List<object[]> _data = new List<object[]>
    {
        new object[] { "hello world", 'w', 6 },
        new object[] { "goodnight moon", 'w', -1 }
    };

    public IEnumerator<object[]> GetEnumerator()
    { return _data.GetEnumerator(); }

    IEnumerator IEnumerable.GetEnumerator()
    { return GetEnumerator(); }
}
quetzalcoatl
źródło
@dcastro: tak, właściwie szukam niektórych w oryginalnych dokumentach xunit
quetzalcoatl
2
@Nick: Zgadzam się, że jest podobny do PropertyData, ale również masz wskazał powód: static. Właśnie dlatego nie zrobiłbym tego. ClassData jest wtedy, gdy chcesz uciec od statyki. W ten sposób można łatwiej ponownie wykorzystać (tj. Zagnieżdżać) generatory.
quetzalcoatl
1
Jakieś pomysły, co się stało z ClassData? Nie mogę go znaleźć w xUnit2.0, na razie używam MemberData z metodą statyczną, która tworzy nowe wystąpienie klasy i zwraca to.
Erti-Chris Eelmaa
14
@Erti, użyj, [MemberData("{static member}", MemberType = typeof(MyClass))]aby zamienić ClassDataatrybut.
Junle Li
7
Od C # 6 zalecane jest użycie nameofsłowa kluczowego zamiast sztywnego kodowania nazwy właściwości (łatwo się psuje, ale cicho).
sara,
41

Aby zaktualizować odpowiedź @ Quetzalcoatl: Atrybut [PropertyData]został zastąpiony przez, [MemberData]który przyjmuje jako argument nazwę ciągu dowolnej metody statycznej, pola lub właściwości, która zwraca IEnumerable<object[]>. (Uważam, że szczególnie fajnie jest mieć metodę iteratora, która może faktycznie obliczać przypadki testowe pojedynczo, uzyskując je w miarę ich obliczania).

Każdy element w sekwencji zwracany przez moduł wyliczający jest object[]i każda tablica musi mieć taką samą długość i ta długość musi być liczbą argumentów dla twojego przypadku testowego (z adnotacją z atrybutem [MemberData]i każdy element musi mieć ten sam typ co odpowiadający mu parametr metody (A może mogą to być typy zamienne, nie wiem).

(Zobacz informacje o wydaniu xUnit.net z marca 2014 r. I aktualną poprawkę z przykładowym kodem ).

davidbak
źródło
3
@davidbak Codplex zniknął. Link nie działa
Kishan Vaishnav
10

Załóżmy, że mamy złożoną klasę samochodu, która ma klasę producenta:

public class Car
{
     public int Id { get; set; }
     public long Price { get; set; }
     public Manufacturer Manufacturer { get; set; }
}
public class Manufacturer
{
    public string Name { get; set; }
    public string Country { get; set; }
}

Zamierzamy wypełnić i zdać klasę samochodów do testu teorii.

Stwórz więc klasę „CarClassData”, która zwraca instancję klasy Car, jak poniżej:

public class CarClassData : IEnumerable<object[]>
    {
        public IEnumerator<object[]> GetEnumerator()
        {
            yield return new object[] {
                new Car
                {
                  Id=1,
                  Price=36000000,
                  Manufacturer = new Manufacturer
                  {
                    Country="country",
                    Name="name"
                  }
                }
            };
        }
        IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
    }

Pora na stworzenie metody testowej (CarTest) i zdefiniowanie samochodu jako parametru:

[Theory]
[ClassData(typeof(CarClassData))]
public void CarTest(Car car)
{
     var output = car;
     var result = _myRepository.BuyCar(car);
}

typ złożony w teorii

Powodzenia

Iman Bahrampour
źródło
3
Ta odpowiedź wyraźnie odnosi się do kwestii przekazania typu niestandardowego jako danych wejściowych teorii, których wydaje się brakować w wybranej odpowiedzi.
JD Cain
1
To jest dokładnie ten przypadek użycia, którego szukałem, polegający na przekazaniu typu złożonego jako parametru do teorii. Działa świetnie! To naprawdę się opłaca przy testowaniu wzorców MVP. Mogę teraz ustawić wiele różnych instancji Widoku w różnych stanach i przekazać je wszystkie do tej samej Teorii, która testuje efekty, jakie metody Prezentera mają na ten widok. Kocham to!
Denis M. Kitchen
10

Tworzenie anonimowych tablic obiektów nie jest najłatwiejszym sposobem konstruowania danych, więc użyłem tego wzorca w moim projekcie

Najpierw zdefiniuj kilka wspólnych klas wielokrotnego użytku

//http://stackoverflow.com/questions/22093843
public interface ITheoryDatum
{
    object[] ToParameterArray();
}

public abstract class TheoryDatum : ITheoryDatum
{
    public abstract object[] ToParameterArray();

    public static ITheoryDatum Factory<TSystemUnderTest, TExpectedOutput>(TSystemUnderTest sut, TExpectedOutput expectedOutput, string description)
    {
        var datum= new TheoryDatum<TSystemUnderTest, TExpectedOutput>();
        datum.SystemUnderTest = sut;
        datum.Description = description;
        datum.ExpectedOutput = expectedOutput;
        return datum;
    }
}

public class TheoryDatum<TSystemUnderTest, TExecptedOutput> : TheoryDatum
{
    public TSystemUnderTest SystemUnderTest { get; set; }

    public string Description { get; set; }

    public TExpectedOutput ExpectedOutput { get; set; }

    public override object[] ToParameterArray()
    {
        var output = new object[3];
        output[0] = SystemUnderTest;
        output[1] = ExpectedOutput;
        output[2] = Description;
        return output;
    }

}

Teraz Twoje indywidualne dane testowe i składowe są łatwiejsze do napisania i wyczyszczenia ...

public class IngredientTests : TestBase
{
    [Theory]
    [MemberData(nameof(IsValidData))]
    public void IsValid(Ingredient ingredient, bool expectedResult, string testDescription)
    {
        Assert.True(ingredient.IsValid == expectedResult, testDescription);
    }

    public static IEnumerable<object[]> IsValidData
    {
        get
        {
            var food = new Food();
            var quantity = new Quantity();
            var data= new List<ITheoryDatum>();

            data.Add(TheoryDatum.Factory(new Ingredient { Food = food }                       , false, "Quantity missing"));
            data.Add(TheoryDatum.Factory(new Ingredient { Quantity = quantity }               , false, "Food missing"));
            data.Add(TheoryDatum.Factory(new Ingredient { Quantity = quantity, Food = food }  , true,  "Valid"));

            return data.ConvertAll(d => d.ToParameterArray());
        }
    }
}

Właściwość string Descriptionpolega na rzucaniu sobie kości, gdy jeden z wielu przypadków testowych zawiedzie

placet
źródło
1
Lubię to; ma rzeczywisty potencjał dla bardzo złożonego obiektu. Muszę zweryfikować walidację na ponad 90 właściwościach. Mogę przekazać prosty obiekt JSON, zdeserializować go i wygenerować dane do iteracji testowej. Dobra robota.
Gustyn
1
czy parametry metody IsValid Testmethod nie są pomieszane - czy nie powinno to być IsValid (Ingrediant, exprectedResult, testDescription)?
pastacool
3

Możesz spróbować w ten sposób:

public class TestClass {

    bool isSaturday(DateTime dt)
    {
       string day = dt.DayOfWeek.ToString();
       return (day == "Saturday");
    }

    [Theory]
    [MemberData("IsSaturdayIndex", MemberType = typeof(TestCase))]
    public void test(int i)
    {
       // parse test case
       var input = TestCase.IsSaturdayTestCase[i];
       DateTime dt = (DateTime)input[0];
       bool expected = (bool)input[1];

       // test
       bool result = isSaturday(dt);
       result.Should().Be(expected);
    }   
}

Utwórz kolejną klasę do przechowywania danych testowych:

public class TestCase
{
   public static readonly List<object[]> IsSaturdayTestCase = new List<object[]>
   {
      new object[]{new DateTime(2016,1,23),true},
      new object[]{new DateTime(2016,1,24),false}
   };

   public static IEnumerable<object[]> IsSaturdayIndex
   {
      get
      {
         List<object[]> tmp = new List<object[]>();
            for (int i = 0; i < IsSaturdayTestCase.Count; i++)
                tmp.Add(new object[] { i });
         return tmp;
      }
   }
}
Sandy_Vu
źródło
1

Na moje potrzeby chciałem po prostu przeprowadzić serię „użytkowników testowych” przez niektóre testy - ale [ClassData] itp. Wydawało mi się przesadą w stosunku do tego, czego potrzebowałem (ponieważ lista elementów była zlokalizowana dla każdego testu).

Zrobiłem więc co następuje, z tablicą wewnątrz testu - indeksowaną z zewnątrz:

[Theory]
[InlineData(0)]
[InlineData(1)]
[InlineData(2)]
[InlineData(3)]
public async Task Account_ExistingUser_CorrectPassword(int userIndex)
{
    // DIFFERENT INPUT DATA (static fake users on class)
    var user = new[]
    {
        EXISTING_USER_NO_MAPPING,
        EXISTING_USER_MAPPING_TO_DIFFERENT_EXISTING_USER,
        EXISTING_USER_MAPPING_TO_SAME_USER,
        NEW_USER

    } [userIndex];

    var response = await Analyze(new CreateOrLoginMsgIn
    {
        Username = user.Username,
        Password = user.Password
    });

    // expected result (using ExpectedObjects)
    new CreateOrLoginResult
    {
        AccessGrantedTo = user.Username

    }.ToExpectedObject().ShouldEqual(response);
}

To osiągnęło mój cel, zachowując przy tym jasny cel testu. Musisz tylko zsynchronizować indeksy, ale to wszystko.

Wygląda ładnie w wynikach, można ją zwijać i możesz ponownie uruchomić konkretną instancję, jeśli pojawi się błąd:

wprowadź opis obrazu tutaj

Simon_Weaver
źródło
„Dobrze wygląda w wynikach, można ją zwijać i można ponownie uruchomić określoną instancję, jeśli pojawi się błąd”. Bardzo dobra uwaga. Główną wadą MemberDatawydaje się być to, że nie można wyświetlić ani uruchomić testu z określonym wejściem testowym. To jest do bani.
Oliver Pearmain
Właściwie właśnie ustaliłem, że jest to możliwe, MemberDatajeśli używasz TheoryDatai opcjonalnie IXunitSerializable. Więcej informacji i przykładów tutaj ... github.com/xunit/xunit/issues/429#issuecomment-108187109
Oliver Pearmain
1

Tak rozwiązałem twój problem, miałem ten sam scenariusz. Tak więc w linii z obiektami niestandardowymi i inną liczbą obiektów w każdym przebiegu.

    [Theory]
    [ClassData(typeof(DeviceTelemetryTestData))]
    public async Task ProcessDeviceTelemetries_TypicalDeserialization_NoErrorAsync(params DeviceTelemetry[] expected)
    {
        // Arrange
        var timeStamp = DateTimeOffset.UtcNow;

        mockInflux.Setup(x => x.ExportTelemetryToDb(It.IsAny<List<DeviceTelemetry>>())).ReturnsAsync("Success");

        // Act
        var actual = await MessageProcessingTelemetry.ProcessTelemetry(JsonConvert.SerializeObject(expected), mockInflux.Object);

        // Assert
        mockInflux.Verify(x => x.ExportTelemetryToDb(It.IsAny<List<DeviceTelemetry>>()), Times.Once);
        Assert.Equal("Success", actual);
    }

Więc to jest mój test jednostkowy, zwróć uwagę na parametr params . Pozwala to na przesłanie innej ilości obiektów. A teraz moja klasa DeviceTelemetryTestData :

    public class DeviceTelemetryTestData : IEnumerable<object[]>
    {
        public IEnumerator<object[]> GetEnumerator()
        {
            yield return new object[] { new DeviceTelemetry { DeviceId = "asd" }, new DeviceTelemetry { DeviceId = "qwe" } };
            yield return new object[] { new DeviceTelemetry { DeviceId = "asd" }, new DeviceTelemetry { DeviceId = "qwe" } };
            yield return new object[] { new DeviceTelemetry { DeviceId = "asd" }, new DeviceTelemetry { DeviceId = "qwe" } };
            yield return new object[] { new DeviceTelemetry { DeviceId = "asd" }, new DeviceTelemetry { DeviceId = "qwe" } };
        }

        IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
    }

Mam nadzieję, że to pomoże !

Max_Thom
źródło
-1

Myślę, że tu się mylisz. Co Theorywłaściwie oznacza atrybut xUnit : Chcesz przetestować tę funkcję, wysyłając specjalne / losowe wartości jako parametry, które otrzymuje ta funkcja poddawana testowi. Oznacza to, że co można zdefiniować jako kolejnego atrybutu, takich jak: InlineData, PropertyData, ClassData, etc .. będzie źródłem dla tych parametrów. Oznacza to, że należy skonstruować obiekt źródłowy, aby zapewnić te parametry. W twoim przypadku myślę, że powinieneś użyć ClassDataobiektu jako źródła. Ponadto - należy pamiętać, że ClassDatadziedziczy z: IEnumerable<>- oznacza to, że za każdym razem inny zestaw wygenerowanych parametrów będzie używany jako parametry przychodzące do testowanej funkcji, aż do IEnumerable<>uzyskania wartości.

Przykład tutaj: Tom DuPont .NET

Przykład może być nieprawidłowy - nie korzystałem z xUnit przez długi czas

Jaspis
źródło