« Tests unitaires » : différence entre les versions
(6 versions intermédiaires par le même utilisateur non affichées) | |||
Ligne 93 : | Ligne 93 : | ||
public static class MyClassTests | public static class MyClassTests | ||
{ | { | ||
private MyClass | private static MyClass myObj = new MyClass(); | ||
[Fact] | [Fact] | ||
public void MyMethod_EmptyString_Zero() | public static void MyMethod_EmptyString_Zero() | ||
{ | { | ||
Assert.Equal(0, myObj.MyMethod("")); | Assert.Equal(0, myObj.MyMethod("")); | ||
Ligne 107 : | Ligne 102 : | ||
[Fact] | [Fact] | ||
public void MyMethod_NullString_ThrowArgumentException() | public static void MyMethod_NullString_ThrowArgumentException() | ||
{ | { | ||
Assert.Throws<ArgumentException>(() => myObj.MyMethod(null)); | Assert.Throws<ArgumentException>(() => myObj.MyMethod(null)); | ||
Ligne 113 : | Ligne 108 : | ||
[Fact] | [Fact] | ||
public async Task MyMethodAsync_NullString_ThrowArgumentException() | public async static Task MyMethodAsync_NullString_ThrowArgumentException() | ||
{ | { | ||
await Assert.ThrowsAsync<ArgumentException>(() => myObj.MyMethodAsync(null)); | await Assert.ThrowsAsync<ArgumentException>(() => myObj.MyMethodAsync(null)); | ||
Ligne 255 : | Ligne 250 : | ||
== [https://fluentassertions.com/objectgraphs/ Object graph comparison] == | == [https://fluentassertions.com/objectgraphs/ Object graph comparison] == | ||
Compare recursively (on 10 levels) the properties of 2 objects. | {{info | Compare recursively (on 10 levels) the properties of 2 objects.}} | ||
<kode lang='cs'> | <kode lang='cs'> | ||
myObject.Should().BeEquivalentTo(myOtherObject); | myObject.Should().BeEquivalentTo(myOtherObject); | ||
// disable recursion | myObject.Should().BeEquivalentTo(myOtherObject, options => options.AllowingInfiniteRecursion()); // infinite recursion | ||
myObject.Should().BeEquivalentTo(myOtherObject, options => options. | myObject.Should().BeEquivalentTo(myOtherObject, options => options.ExcludingNestedObjects()); // disable recursion | ||
myObject.Should().BeEquivalentTo(myOtherObject, options => options.Exclude(x => x.Property1)); // exclude Property1 of the comparison | |||
// exclude of the comparison, the Property2 of all the the items | |||
myObject.Should().BeEquivalentTo(myOtherObject, options => options.For(x => x.Items)Exclude(x => x.Property2)); | |||
</kode> | </kode> | ||
Ligne 270 : | Ligne 271 : | ||
= [https://github.com/AutoFixture/AutoFixture AutoFixture] = | = [https://github.com/AutoFixture/AutoFixture AutoFixture] = | ||
<filebox fn='MyUnitTest.cs'> | <filebox fn='MyUnitTest.cs'> | ||
private readonly Fixture fixture = new Fixture(); | private static readonly Fixture fixture = new Fixture(); | ||
[Fact] | [Fact] | ||
public void Test() | public static void Test() | ||
{ | { | ||
// create an instance of MyClass with the whole hierarchy of child properties | // create an instance of MyClass with the whole hierarchy of child properties | ||
Ligne 282 : | Ligne 283 : | ||
.With(x => x.Property1, "Value") | .With(x => x.Property1, "Value") | ||
.Create(); | .Create(); | ||
// create multiple instances (default: 3) | // create multiple instances (default: 3), here 10 | ||
var myObjects = fixture.CreateMany<MyClass>(); | var myObjects = fixture.CreateMany<MyClass>(10); | ||
// define customization conventions | // define customization conventions |
Dernière version du 12 septembre 2024 à 14:30
Links
- Unit testing best practices with .NET Core and .NET Standard
- Entity Framework unit tests with EFFORT
Convention de nommage
Assembly naming | MyProject.Tests |
Unit test class naming | MyClassTest |
Unit test method naming | FeatureBeingTested_StateUnderTest_ExpectedBehavior |
Test Driven Development
- Transcrire les spécification en tests unitaires
- Créer les classes business pour permettre la compilation
- À ce stage les tests échouent
- Créer la logique business afin que les tests soient validés
- Refactoriser le code
AAA Syntaxe
- Arrange: initialiser l'élément à tester, créer le mock object et le passer à l'élément à tester
- Act: exécuter le test
- Assert: vérifier que le résultat du test correspond à ce qui est attendu
Visual Studio Unit Testing Framework / MS Test Framework
Création d'un projet de tests unitaires
- Ajouter un nouveau projet de type Test → Unit Test Project → MonProjet.Test
- Ajouter une référence à MonProjet
Code
Accèder aux éléments privée d'une autre assembly
using Microsoft.VisualStudio.TestTools.UnitTesting; using MonProjet; namespace MonProjet.Test { [TestClass] public class UnitTest1 { [TestMethod] public void TestMethod1() { var myObject = new MaClasse(); var résultat = myObject.MaMéthode(); Assert.IsTrue(résultat == 111); |
Lancer les tests
- Test → Windows → Test Explorer
- Run
Debuger les tests
- Placer les breakpoint
- Clique-droit sur le test → Debug Selected Tests
NUnit
Création d'un projet de tests unitaires
- Ajouter un nouveau projet de type NUnit Library Project → MonProjetTest
- Ajouter une référence à MonProjet
Code
using NUnit.Framework; using MonProjet; namespace MonProjetTest { [TestFixture()] public class Test { [Test()] public void MaMéthodeTest() { var o = new MaClasse(); var résultat = o.MaMéthode(); Assert.AreEqual(111, résultat); } } |
xUnit
dotnet new xunit -o MyProject.Tests dotnet sln add ./MyProject.Tests/MyProject.Tests.csproj |
MyProject.Tests/MyClassTests.cs |
public static class MyClassTests { private static MyClass myObj = new MyClass(); [Fact] public static void MyMethod_EmptyString_Zero() { Assert.Equal(0, myObj.MyMethod("")); } [Fact] public static void MyMethod_NullString_ThrowArgumentException() { Assert.Throws<ArgumentException>(() => myObj.MyMethod(null)); } [Fact] public async static Task MyMethodAsync_NullString_ThrowArgumentException() { await Assert.ThrowsAsync<ArgumentException>(() => myObj.MyMethodAsync(null)); } |
Naming: Method name Input Expected output |
Steps:
|
Assert
// vérifie que myList ne contient aucun item Assert.Empty(myList); // vérifie que myList ne contient qu'un seul item Assert.Single(myList); // vérifie que myList contient 2 items Assert.Collection( myList, item => Assert.NotNull(item.Prop1), // vérifie que Prop1 du 1er item n'est pas null item => Assert.Null(item.Prop1), // vérifie que Prop1 du 2ème item est null // vérifie la propriété Prop1 n'est pas null pour tous les items de myList Assert.All(myList, item => Assert.NotNull(item.Prop1)); // vérifie qu'une liste ne contient pas une valeur Assert.DoesNotContain(myValue, myList); |
Theory Data
- Creating parameterised tests in xUnit with InlineData, ClassData, and MemberData
- Creating strongly typed xUnit theory test data with TheoryData
Member data
public static TheoryData<int, int, int> DataToTest => new() { {1, 1, 2}, {5, 2, 7} }; public static IEnumerable<object[]> DataToTest => new[] { new object[] { 1, 1, 2 }, new object[] { 5, 2, 7 } }; [Theory] [MemberData(nameof(DataToTest))] public void Add_DataToTest_ExpectedResult(int firstOperand, int secondOperand, int expectedResult) { var math = new MyMathClass(); var result = math.Add(firstOperand, secondOperand); Assert.Equals(expectedResult, result); } |
Inline data
[Theory] [InlineData("-1")] [InlineData("1")] [InlineData("9.8765")] public void MyMethod_NumberAsString_DecimalDifferentFromZero(string value) { var result = myObj.MyMethod(value); Assert.NotEqual(0, result); } |
How to generate theory data
[Theory] [MemberData(nameof(MyMethodTestData))] public void MyMethod_Input_Output(string input, MyClass expectedResult) { } public static IEnumerable<object[]> MyMethodTestData => new List<object[]> { new object[] { "input", CreateMyClass(x => x.Prop1 = "value") } }; // create duplicates of MyClass instance with modifications private static MyClass CreateMyClass(Action<MyClass> update) { var object = new MyClass { Prop1 = "value1", Prop2 = "value2" }; update(object); return object; } |
Installation pour VSCode et .net core
# créer la solution md MaSolution cd MaSolution dotnet new sln # créer le projet à tester md MyProject cd MyProject dotnet new console # ajouter le projet à la solution cd .. dotnet sln add MyProject/MyProject.csproj # créer le projet de test dotnet new xunit -o MyProject.Tests # ajouter une référence au projet à tester dotnet add reference ../MyProject/MyProject.csproj # ajouter le projet à la solution cd .. dotnet sln add MyProject.Tests/MyProject.Tests.csproj |
Extension: .NET Core Test Explorer
L'utilisation d'un fichier de solution *.sln permet de builder tous les projets de la solution en même temps. |
Fluent assertion
# add the nuget package dotnet add package FluentAssertions |
Object graph comparison
Compare recursively (on 10 levels) the properties of 2 objects. |
myObject.Should().BeEquivalentTo(myOtherObject); myObject.Should().BeEquivalentTo(myOtherObject, options => options.AllowingInfiniteRecursion()); // infinite recursion myObject.Should().BeEquivalentTo(myOtherObject, options => options.ExcludingNestedObjects()); // disable recursion myObject.Should().BeEquivalentTo(myOtherObject, options => options.Exclude(x => x.Property1)); // exclude Property1 of the comparison // exclude of the comparison, the Property2 of all the the items myObject.Should().BeEquivalentTo(myOtherObject, options => options.For(x => x.Items)Exclude(x => x.Property2)); |
Collection
largerCollection.Should().Contain(smallerCollection); |
AutoFixture
MyUnitTest.cs |
private static readonly Fixture fixture = new Fixture(); [Fact] public static void Test() { // create an instance of MyClass with the whole hierarchy of child properties var myObject = fixture.Create<MyClass>(); // customize the value of properties var myObject = fixture.Build<MyClass>() .With(x => x.Property1, "Value") .Create(); // create multiple instances (default: 3), here 10 var myObjects = fixture.CreateMany<MyClass>(10); // define customization conventions fixture.Customize<MyClass>(c => c.With(x => x.Property1, "Value") .With(x => x.Property2, 10)); fixture.Customize(new MyClassConventions()); } public class MyClassConventions : ICustomization { public void Customize(IFixture fixture) { fixture.Customize<MyClass>(x => x.With(y => y.Property1, "Value")); } } |
NBuilder
Test object generator.
No deep generation of objects. |
// build an item and fill it with data var item = Builder<Item>.CreateNew() .Build(); // build 10 items and fill them with data var items = Builder<Item>.CreateListOfSize(10) .Build(); // fill name with a static value var items = Builder<Item>.CreateListOfSize(10) .All() .With(x => x.Name = "Name") .Build(); // force to use the ctor with User parameter var items = Builder<Item>.CreateListOfSize(10) .All() .WithFactory(() => new Item(Builder<User>.CreateNew().Build())) .Build(); |
MOQ
Permet la création de Fake Object afin de remplacer les dépendances de l'objet que l'on souhaite tester.
Installer avec NuGet Moq.
// créé un fake object DataService var mockDataService = new Mock<IDataService>(); // le fake object DataService IDataService dataService = mockDataService.Object; // appel du ctor avec la dépendance à IDataService var myObj = new MyClass(dataService); // exécution de la méthode à tester myObj.MethodToTest(); |
On veut tester la méthode MethodToTest de la classe MyClass.
Cette méthode fait appel à un objet DataService que nous n'avons pas besoin de tester.
On va donc créer un mock de DataService pour permettre l'exécution de la méthode MethodToTest.
public class MyClass { private IDataService ds; public MyClass(IDataService ds) { this.ds = ds; } public void MethodToTest() { // some code this.ds.GetPersons(); // some code } } public class DataService : IDataService { List<Person> GetPersons() { /* ... */ } } |
AssemblyInfo.cs |
// Si MOQ n'arrive pas à accéder aux éléments internes [assembly: InternalsVisibleTo("DynamicProxyGenAssembly2")] |
Methods
// définit ce que la méthode GetPersons renvoie var persons = new List<Person> { /* ... */ }; mockDataService.Setup(ds => ds.GetPersons()) .Returns(() => persons); // définit ce que la méthode GetPersonById renvoie var person = new Person { /* ... */ }; // avec un int positif comme argument mockDataService.Setup(ds => ds.GetPersonById(It.Is<int>(i => i >= 0)) .Returns(() => person); // avec un int négatif comme argument mockDataService.Setup(ds => ds.GetPersonById(It.Is<int>(i => i < 0))) .Throws<ArgumentException>(); // utiliser le paramètre passé en argument mockDataService.Setup(ds => ds.GetPersonById(It.Is<int>())) .Returns((int id) => personList[i]); // arguments It.IsIn(1, 2, 3); It.IsInRange(1, 9, Range.Inclusive); It.IsRegex("[1-9]"); |
Async method
public async Task<string> MethodAsync() { /* ... */ }; fakeService.Setup(x => x.MethodAsync()) .ReturnsAsync("Result"); //.Returns(Task.FromResult("Result")); |
Sequence
Permet de renvoyer des résultats différent pour une même méthode.
mockDataService.SetupSequence(ds => ds.GetPersons()) .Returns(() => firstListOfpersons) // the first list is returned during the first call .Returns(() => secondListOfpersons); // the second list is returned during the second call |
Properties
var persons = new List<Person> { /* ... */ }; mockDataService.SetupProperty(m => m.Persons, persons); mockDataService.SetupAllProperties(); mockDataService.Object.Persons = persons; |
Par défaut, les propriétés d'un mock ne conservent pas leurs modifications durant le test. Pour ce faire il faut utiliser SetupProperty. |
Mock property with a private setter
// property with a private setter public string PropertyToMock { get; } fakeService.SetupGet(x => x.PropertyToMock).Returns("ExpectedValue"); |
Indexed property
var mockService = new Mock<IService>(); mockService.SetupGet(x => x[It.IsAny<string>()]).Returns("123"); var value = mockService.Object["any"]; // "123" |
Events
// event EventHandler<MyCustomEventArgs> NotifyManager mockDataService.Raise(m => m.NotifyManager += null, new MyCustomEventArgs("message")); // sans arguments: EventArgs.Empty // public delegate void NotifyManagerDelegate(string message, bool requireAnswer) // event NotifyManagerDelegate NotifyManager mockDataService.Raise(m => m.NotifyManager += null, "message", true); |
Verify
Permet de savoir si une méthode ou une propriété a été appelée.
// vérifier que la méthode GetPersons a été appelée mockDataService.Verify(ds => ds.GetPersons(), "Custom error message"); // vérifier que la méthode GetPersonById a été appelée avec l'argument 1 mockDataService.Verify(m => m.GetPersonById( It.Is<int>(fn => fn.Equals(1)))); // vérifier que la méthode GetPersons a été appelée 2 fois mockDataService.Verify(ds => ds.GetPersons(), Times.Exactly(2)); // vérifier que la méthode GetPersons n'a jamais été appelée mockDataService.Verify(ds => ds.GetPersons(), Times.Never); // vérifier que la propriété Persons a bien été settée mockDataService.VerifySet(m => m.Persons = It.IsAny<IList<Person>>()); // vérifier que la propriété Persons a bien été gettée mockDataService.VerifyGet(m => m.Persons); |
Strict / Loose Mocking
Strict | lance une exception si un membre de l'objet est appelée sans avoir été définie Setup. |
Loose | ne lance pas d'exceptions et retourne la valeur par défaut. |
// Par défaut c'est le comportement Loose qui est utilisé var mockDataService = new Mock<IDataService>(MockBehavior.Strict); |
Récursive Mocking
Permet d'accéder au mock résultant d'un membre de l'objet sans avoir à le définir manuellement.
// les méthodes et propriétés retourne des mocks si possible au lieu de null var mockDataService = new Mock<IDataService>() { DefaultValue = DefaultValue.Mock }; IMyResult result = mockDataService.Object.MyMethod(It.IsAny<string>()); Mock<IMyResult> mockResult = Mock.Get(result); // vérifier que IMyResult.MyOtherMethod est bien appelé lors de l'exécution de IDataService.MyMethod // sans avoir besoin de définir manuellement un mock pour IMyResult mockResult.Verify(m => m.MyOtherMethod()); |
Avec DefaultValue.Mock, Moq créé automatiquement un mock pour les interfaces, les classes abstraires et les classes non-sealed. |
Mock Repository
Permet de mutualiser la configuration et la vérification des mock.
var mockFactory = new MockRepository(MockBehavior.Strick) { DefaultValue = DefaultValue.Mock }; // création des mock avec la même configuration var mockDataService1 = mockFactory.Create<IDataService1>(); var mockDataService2 = mockFactory.Create<IDataService2>(); // vérification de tous les mock en une seule ligne mockFactory.Verify(); |
Mock HttpClient
var httpClient = new HttpClient(new MockHttpMessageHandler()); private sealed class MockHttpMessageHandler : HttpMessageHandler { private const string json = /*lang=json,strict*/ @"..."; protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) => Task.FromResult(new HttpResponseMessage { StatusCode = HttpStatusCode.OK, Content = new StringContent(json, Encoding.UTF8, "application/json") }); } |
Membres protected
Permet de mocker un membre protected.
using Moq.Protected; var mockDataService = new Mock<IDataService>(); // Setup<string> type de retour // argument 1 : ItExpr.IsAny<string>, Argument 2 : ItExpr.IsAny<int> // Verifiable : rend le membre vérifiable mockDataService.Protected() .Setup<string>("MyProtectedMethod", ItExpr.IsAny<string>, ItExpr.IsAny<int>) .Returns("1234") .Verifiable(); mockDataService.Verify(); |
Mocking extension methods
Even if at compile time the extension method can be found, at runtime the mocking framework will have issues trying to mock it since it doesn't belong to the ISession type. |
Since it is not possible to mock extension methods, try to mock the methods called in the extension method you want to mock.
NSubstitute
.NET mocking libraries