Passer des paramètres complexes à [Théorie]


98

Xunit a une fonctionnalité intéressante : vous pouvez créer un test avec un Theoryattribut et mettre des données dans des InlineDataattributs, et xUnit générera de nombreux tests et les testera tous.

Je veux avoir quelque chose comme ça, mais les paramètres à ma méthode ne sont pas « données simples » (comme string, int, double), mais une liste de ma classe:

public static void WriteReportsToMemoryStream(
    IEnumerable<MyCustomClass> listReport,
    MemoryStream ms,
    StreamWriter writer) { ... }

3
Si cela a du sens dans votre environnement, vous pouvez le faire en F # avec beaucoup moins de bruit: - stackoverflow.com/a/35127997/11635
Ruben Bartelink

1
Un guide complet qui envoie des objets complexes en tant que paramètre aux types complexes de
Iman Bahrampour

Réponses:


137

Il existe de nombreux xxxxDataattributs dans XUnit. Découvrez par exemple l' PropertyDataattribut.

Vous pouvez implémenter une propriété qui retourne IEnumerable<object[]>. Chacun object[]que cette méthode génère sera ensuite "décompressé" en tant que paramètre pour un seul appel à votre [Theory]méthode.

Une autre option est ClassData, qui fonctionne de la même manière, mais permet de partager facilement les «générateurs» entre les tests dans différentes classes / espaces de noms, et sépare également les «générateurs de données» des méthodes de test réelles.

Voir ie ces exemples à partir d'ici :

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

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

@dcastro: ouais, j'en cherche en fait sur les documents originaux de xunit
quetzalcoatl

2
@ Nick: Je suis d' accord qui est similaire à PropertyData, mais aussi, vous l' avez souligné la raison: static. C'est exactement pourquoi je ne le ferais pas. ClassData est lorsque vous souhaitez échapper à la statique. Ce faisant, vous pouvez réutiliser (c'est-à-dire imbriquer) les générateurs plus facilement.
quetzalcoatl

1
Des idées sur ce qui s'est passé avec ClassData? Je ne peux pas le trouver dans xUnit2.0, pour l'instant, j'utilise MemberData avec une méthode statique, qui crée une nouvelle instance de classe et la renvoie.
Erti-Chris Eelmaa

14
@Erti, utilisez [MemberData("{static member}", MemberType = typeof(MyClass))]pour remplacer l' ClassDataattribut.
Junle Li

6
Depuis C # 6, il était recommandé d'utiliser le nameofmot - clé au lieu de coder en dur un nom de propriété (se brise facilement mais silencieusement).
sara

40

Pour mettre à jour la réponse de @ Quetzalcoatl: L'attribut [PropertyData]a été remplacé par [MemberData]lequel prend comme argument le nom de chaîne de toute méthode statique, champ ou propriété qui renvoie un IEnumerable<object[]>. (Je trouve particulièrement agréable d'avoir une méthode d'itération qui peut réellement calculer les cas de test un à la fois, les cédant au fur et à mesure qu'ils sont calculés.)

Chaque élément de la séquence retournée par l'énumérateur est un object[]et chaque tableau doit avoir la même longueur et cette longueur doit être le nombre d'arguments de votre scénario de test (annoté avec l'attribut [MemberData]et chaque élément doit avoir le même type que le paramètre de méthode correspondant (Ou peut-être qu'ils peuvent être des types convertibles, je ne sais pas.)

(Voir les notes de publication de xUnit.net de mars 2014 et le correctif actuel avec un exemple de code .)


2
@davidbak Le codplex est parti. Le lien ne fonctionne pas
Kishan Vaishnav

11

La création de tableaux d'objets anonymes n'est pas le moyen le plus simple de construire les données, j'ai donc utilisé ce modèle dans mon projet

Définissez d'abord des classes partagées réutilisables

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

}

Désormais, votre test individuel et vos données de membre sont plus faciles à écrire et à nettoyer ...

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

La Descriptionpropriété string est de vous jeter un os lorsque l'un de vos nombreux cas de test échoue


1
J'aime ça; il a un vrai potentiel pour un objet très complexe que je dois valider les validations sur plus de 90 propriétés. Je peux transmettre un objet JSON simple, le désérialiser et générer les données pour une itération de test. Bon travail.
Gustyn

1
les paramètres de la méthode de test IsValid ne sont-ils pas mélangés - ne devrait-il pas être IsValid (ingrediant, exprectedResult, testDescription)?
pastacool

9

Supposons que nous ayons une classe de voiture complexe qui a une classe de fabricant:

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

Nous allons remplir et passer la classe Car à un test de théorie.

Créez donc une classe 'CarClassData' qui renvoie une instance de la classe Car comme ci-dessous:

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

Il est temps de créer une méthode de test (CarTest) et de définir la voiture comme paramètre:

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

type complexe en théorie

Bonne chance


3
Cette réponse aborde explicitement la question de la transmission d'un type personnalisé comme entrée de théorie qui semble être absente de la réponse sélectionnée.
JD Cain

1
C'est exactement le cas d'utilisation que je recherchais: comment passer un type complexe en tant que paramètre à une théorie. Fonctionne parfaitement! Cela est vraiment rentable pour tester les modèles MVP. Je peux maintenant configurer de nombreuses instances différentes d'une vue dans toutes sortes d'états et les passer toutes dans la même théorie qui teste les effets des méthodes Presenter sur cette vue. Aimer!
Denis M. Kitchen

3

Vous pouvez essayer de cette façon:

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

Créez une autre classe pour contenir les données de test:

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

1

Pour mes besoins, je voulais juste lancer une série d '«utilisateurs de test» à travers quelques tests - mais [ClassData] etc. semblait exagéré pour ce dont j'avais besoin (car la liste des éléments était localisée à chaque test).

J'ai donc fait ce qui suit, avec un tableau à l'intérieur du test - indexé de l'extérieur:

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

Cela a atteint mon objectif, tout en gardant clairement l'intention du test. Il vous suffit de synchroniser les index, mais c'est tout.

Cela a l'air bien dans les résultats, il est pliable et vous pouvez réexécuter une instance spécifique si vous obtenez une erreur:

entrez la description de l'image ici


"Cela a l'air bien dans les résultats, il est pliable et vous pouvez réexécuter une instance spécifique si vous obtenez une erreur". Très bon point. Un inconvénient majeur de MemberDatasemble être que vous ne pouvez pas voir ni exécuter le test avec une entrée de test spécifique. Ça craint.
Oliver Pearmain le

En fait, je viens de comprendre que c'est possible avec MemberDatasi vous utilisez TheoryDataet éventuellement IXunitSerializable. Plus d'informations et d'exemples ici ... github.com/xunit/xunit/issues/429#issuecomment-108187109
Oliver Pearmain

1

C'est ainsi que j'ai résolu votre problème, j'ai eu le même scénario. Donc en ligne avec des objets personnalisés et un nombre différent d'objets à chaque exécution.

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

C'est donc mon test unitaire, notez le paramètre params . Cela permet d'envoyer un nombre d'objets différent. Et maintenant ma classe 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();
    }

J'espère que ça aide !


-1

Je suppose que vous vous êtes trompé ici. Ce que Theorysignifie réellement l'attribut xUnit : Vous souhaitez tester cette fonction en envoyant des valeurs spéciales / aléatoires en tant que paramètres reçus par cette fonction sous test. Cela signifie que ce que vous définissez comme l'attribut suivant, par exemple: InlineData, PropertyData, ClassData, etc .. seront la source de ces paramètres. Cela signifie que vous devez construire l'objet source pour fournir ces paramètres. Dans votre cas, je suppose que vous devriez utiliser l' ClassDataobjet comme source. Aussi - veuillez noter que ClassDatahérite de: IEnumerable<>- cela signifie que chaque fois qu'un autre ensemble de paramètres générés sera utilisé comme paramètres entrants pour la fonction en cours de test jusqu'à ce que IEnumerable<>produise des valeurs.

Exemple ici: Tom DuPont .NET

L'exemple peut être incorrect - Je n'ai pas utilisé xUnit pendant une longue période

En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.