Übergeben Sie komplexe Parameter an [Theorie]

98

Xunit hat eine nette Funktion : Sie können einen Test mit einem TheoryAttribut erstellen und Daten in InlineDataAttribute einfügen , und xUnit generiert viele Tests und testet sie alle.

Ich möchte so etwas haben, aber die Parameter auf meine Methode nicht ‚einfach Daten‘ (wie string, int, double), aber eine Liste meiner Klasse:

public static void WriteReportsToMemoryStream(
    IEnumerable<MyCustomClass> listReport,
    MemoryStream ms,
    StreamWriter writer) { ... }
zchpit
quelle
3
Wenn es in Ihrer Umgebung Sinn macht, können Sie es in F # mit viel weniger Rauschen tun: - stackoverflow.com/a/35127997/11635
Ruben Bartelink
1
Eine vollständige Anleitung, die komplexe Objekte als Parameter an komplexe
sendet

Antworten:

137

xxxxDataIn XUnit gibt es viele Attribute. Schauen Sie sich zum Beispiel das PropertyDataAttribut an.

Sie können eine Eigenschaft implementieren, die zurückgibt IEnumerable<object[]>. Jedes object[], das diese Methode generiert, wird dann als Parameter für einen einzelnen Aufruf Ihrer [Theory]Methode "entpackt" .

Eine andere Option ist ClassData, die gleich funktioniert, aber die einfache Freigabe der 'Generatoren' zwischen Tests in verschiedenen Klassen / Namespaces ermöglicht und auch die 'Datengeneratoren' von den tatsächlichen Testmethoden trennt.

Siehe zB diese Beispiele von hier :

PropertyData-Beispiel

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 }
            };
        }
    }
}

ClassData Beispiel

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
quelle
@dcastro: Ja, ich suche tatsächlich nach einigen auf Original-Xunit-Dokumenten
Quetzalcoatl
2
@ Nick: Ich bin damit einverstanden, dass dies PropertyData ähnelt, aber Sie haben auch den Grund dafür angegeben : static. Genau deshalb würde ich nicht. ClassData ist, wenn Sie der Statik entkommen möchten. Auf diese Weise können Sie die Generatoren einfacher wiederverwenden (dh verschachteln).
Quetzalcoatl
1
Irgendwelche Ideen, was mit ClassData passiert ist? Ich kann es in xUnit2.0 nicht finden. Derzeit verwende ich MemberData mit einer statischen Methode, die eine neue Klasseninstanz erstellt und diese zurückgibt.
Erti-Chris Eelmaa
14
@Erti, verwenden Sie [MemberData("{static member}", MemberType = typeof(MyClass))], um das ClassDataAttribut zu ersetzen .
Junle Li
6
Ab C # 6 wurde empfohlen, das nameofSchlüsselwort zu verwenden, anstatt einen Eigenschaftsnamen fest zu codieren (bricht leicht, aber lautlos ab).
Sara
40

So aktualisieren Sie die Antwort von @ Quetzalcoatl: Das Attribut [PropertyData]wurde ersetzt, durch [MemberData]das der Zeichenfolgenname einer statischen Methode, eines Felds oder einer Eigenschaft, die eine zurückgibt, als Argument verwendet wird IEnumerable<object[]>. (Ich finde es besonders schön, eine Iterator-Methode zu haben, mit der Testfälle einzeln berechnet werden können, sodass sie bei der Berechnung angezeigt werden.)

Jedes Element in der vom Enumerator zurückgegebenen Sequenz ist ein object[]und jedes Array muss dieselbe Länge haben und diese Länge muss die Anzahl der Argumente für Ihren Testfall sein (mit dem Attribut versehen [MemberData]und jedes Element muss denselben Typ wie der entsprechende Methodenparameter haben (Oder vielleicht können es konvertierbare Typen sein, ich weiß es nicht.)

(Siehe Versionshinweise für xUnit.net März 2014 und den aktuellen Patch mit Beispielcode .)

Davidbak
quelle
2
@davidbak Der Codplex ist weg. Der Link funktioniert nicht
Kishan Vaishnav
11

Das Erstellen anonymer Objektarrays ist nicht der einfachste Weg, um die Daten zu erstellen. Daher habe ich dieses Muster in meinem Projekt verwendet

Definieren Sie zunächst einige wiederverwendbare, gemeinsam genutzte Klassen

//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;
    }

}

Jetzt sind Ihre individuellen Test- und Mitgliedsdaten einfacher zu schreiben und sauberer ...

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());
        }
    }
}

Die DescriptionEigenschaft string besteht darin, sich einen Knochen zuzuwerfen, wenn einer Ihrer vielen Testfälle fehlschlägt

Fiat
quelle
1
Ich mag das; Es hat ein echtes Potenzial für ein sehr komplexes Objekt. Ich muss die Validierungen für mehr als 90 Eigenschaften validieren. Ich kann ein einfaches JSON-Objekt übergeben, es deserialisieren und die Daten für eine Testiteration generieren. Gut gemacht.
Gustyn
1
Sind die Parameter für die IsValid-Testmethode nicht verwechselt - sollte es nicht IsValid sein (Ingrediant, ExprectedResult, TestDescription)?
Pastacool
9

Angenommen, wir haben eine komplexe Fahrzeugklasse mit einer Herstellerklasse:

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; }
}

Wir werden die Autoklasse füllen und einem Theorie-Test unterziehen.

Erstellen Sie also eine 'CarClassData'-Klasse, die eine Instanz der Car-Klasse wie folgt zurückgibt:

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();
    }

Es ist Zeit, eine Testmethode (CarTest) zu erstellen und das Auto als Parameter zu definieren:

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

komplexer Typ in der Theorie

Viel Glück

Iman Bahrampour
quelle
3
Diese Antwort befasst sich explizit mit der Frage, ob ein benutzerdefinierter Typ als Theorieeingabe übergeben werden soll, die in der ausgewählten Antwort zu fehlen scheint.
JD Cain
1
Dies ist genau der Anwendungsfall, nach dem ich gesucht habe, um einen komplexen Typ als Parameter an eine Theorie zu übergeben. Funktioniert perfekt! Dies zahlt sich für das Testen von MVP-Mustern wirklich aus. Ich kann jetzt viele verschiedene Instanzen einer Ansicht in allen möglichen Zuständen einrichten und sie alle an dieselbe Theorie übergeben, die die Auswirkungen der Presenter-Methoden auf diese Ansicht testet. Liebe es!
Denis M. Kitchen
3

Sie können dies folgendermaßen versuchen:

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);
    }   
}

Erstellen Sie eine weitere Klasse für die Testdaten:

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
quelle
1

Für meine Bedürfnisse wollte ich nur eine Reihe von 'Testbenutzern' durch einige Tests führen - aber [ClassData] usw. schienen für das, was ich brauchte, übertrieben (da die Liste der Elemente für jeden Test lokalisiert war).

Also habe ich Folgendes mit einem Array innerhalb des Tests gemacht - von außen indiziert:

[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);
}

Dies hat mein Ziel erreicht und gleichzeitig die Absicht des Tests klar gehalten. Sie müssen nur die Indizes synchron halten, aber das ist alles.

Sieht in den Ergebnissen gut aus, ist reduzierbar und Sie können eine bestimmte Instanz erneut ausführen, wenn Sie eine Fehlermeldung erhalten:

Geben Sie hier die Bildbeschreibung ein

Simon_Weaver
quelle
"Sieht in den Ergebnissen gut aus, ist zusammenklappbar und Sie können eine bestimmte Instanz erneut ausführen, wenn Sie einen Fehler erhalten." Sehr guter Punkt. Ein Hauptnachteil von MemberDatascheint zu sein, dass Sie den Test mit einer bestimmten Testeingabe nicht sehen oder ausführen können. Es nervt.
Oliver Pearmain
Eigentlich habe ich gerade herausgefunden, dass es möglich ist, MemberDatawenn Sie TheoryDataund optional verwenden IXunitSerializable. Weitere Infos und Beispiele hier ... github.com/xunit/xunit/issues/429#issuecomment-108187109
Oliver Pearmain
1

So habe ich Ihr Problem gelöst, ich hatte das gleiche Szenario. Also inline mit benutzerdefinierten Objekten und einer anderen Anzahl von Objekten bei jedem Lauf.

    [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);
    }

Dies ist also mein Komponententest . Beachten Sie den Parameter params . Dies ermöglicht das Senden einer anderen Anzahl von Objekten. Und jetzt meine DeviceTelemetryTestData- Klasse:

    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();
    }

Ich hoffe es hilft !

Max_Thom
quelle
-1

Ich denke du hast dich hier geirrt. Was das xUnit- TheoryAttribut tatsächlich bedeutet: Sie möchten diese Funktion testen, indem Sie spezielle / zufällige Werte als Parameter senden, die diese zu testende Funktion empfängt. Das bedeutet , dass , was Sie als nächstes Attribut definieren, wie zum Beispiel: InlineData, PropertyData, ClassData, etc .. wird die Quelle für diese Parameter sein. Das bedeutet, dass Sie das Quellobjekt erstellen sollten, um diese Parameter bereitzustellen. In Ihrem Fall sollten Sie das ClassDataObjekt als Quelle verwenden. Beachten Sie außerdem ClassDataFolgendes: IEnumerable<>- Dies bedeutet, dass jedes Mal ein anderer Satz generierter Parameter als eingehende Parameter für die zu testende Funktion verwendet wird, bis IEnumerable<>Werte erzeugt werden.

Beispiel hier: Tom DuPont .NET

Beispiel kann falsch sein - ich habe xUnit lange nicht benutzt

Jaspis
quelle