Xunit a une fonctionnalité intéressante : vous pouvez créer un test avec un attribut Theory
et placer les données dans des attributs InlineData
. XUnit générera de nombreux tests et les testera tous.
Je veux avoir quelque chose comme ça, mais les paramètres de ma méthode ne sont pas des 'données simples' (comme string
, int
, double
), mais une liste de ma classe:
public static void WriteReportsToMemoryStream(
IEnumerable<MyCustomClass> listReport,
MemoryStream ms,
StreamWriter writer) { ... }
Il existe de nombreux attributs xxxxData
dans XUnit. Découvrez par exemple l'attribut PropertyData
.
Vous pouvez implémenter une propriété qui retourne IEnumerable<object[]>
. Chaque object[]
généré par cette méthode sera ensuite "décompressé" en tant que paramètres pour un seul appel de votre méthode [Theory]
.
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 de 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 i.e. ces exemples d’ici :
PropertyData Exemple
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 de données
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(); }
}
Pour mettre à jour la réponse de @ Quetzalcoatl: L'attribut [PropertyData]
a été remplacé par [MemberData]
, qui prend en argument le nom de chaîne d'une méthode, d'un champ ou d'une propriété statique renvoyant un IEnumerable<object[]>
. (Je trouve particulièrement agréable d’avoir une méthode itérateur qui peut en fait calculer des tests élémentaires, l’un après l’autre, en les renvoyant au fur et à mesure de leur calcul.)
Chaque élément de la séquence renvoyé 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 l'élément correspondant. paramètre de méthode (ou peut-être que ce sont des types convertibles, je ne sais pas).
(Voir Notes de publication de xUnit.net Mars 2014 et Le correctif actuel avec l'exemple de code .)
La création de tableaux d'objets anonymes n'étant pas le moyen le plus simple de construire les données, j'ai utilisé ce modèle dans mon projet.
Commencez par définir des classes réutilisables et partagées
//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, vos données individuelles de test et de membre sont plus faciles à écrire et à nettoyer ...
public class IngredientTests : TestBase
{
[Theory]
[MemberData(nameof(IsValidData))]
public void IsValid(Ingredient ingredient, string testDescription, bool expectedResult)
{
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 propriété string Description
consiste à vous jeter un os lorsque l'un de vos nombreux cas de test échoue
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 de voiture à un test théorique.
Créez donc une classe 'CarClassData' qui retourne 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 en tant que paramètre:
[Theory]
[ClassData(typeof(CarClassData))]
public void CarTest(Car car)
{
var output = car;
var result = _myRepository.BuyCar(car);
}
Bonne chance
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;
}
}
}
Pour mes besoins, je voulais juste exécuter une série d '"utilisateurs de test" à travers certains tests - mais [ClassData], etc. semblait excessif pour ce dont j'avais besoin (car la liste des éléments était localisée pour 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 permis d’atteindre mon objectif tout en maintenant l’intention du test. Il vous suffit de garder les index synchronisés, mais c'est tout.
Cela semble sympa dans les résultats, il est compressible et vous pouvez réexécuter une instance spécifique si vous obtenez une erreur: