« Tests unitaires » : différence entre les versions
(→xUnit) |
(→xUnit) |
||
Ligne 112 : | Ligne 112 : | ||
} | } | ||
} | } | ||
</filebox> | |||
== Theory Data == | |||
<filebox fn='MyProject.Tests/MyClassTests'> | |||
[Theory] | |||
[InlineData("-1")] | |||
[InlineData("1")] | |||
[InlineData("9.8765")] | |||
public void MyMethod_NumberAsString_DecimalDifferentFromZero(string value) | |||
{ | |||
Assert.NotEqual(myObj.MyMethod(value), 0); | |||
} | |||
public static IEnumerable<object[]> InvalidValues | |||
=> new List<object[]> | |||
{ | |||
new object[] { "AAA", 111 }, | |||
new object[] { "AAA", 111 } | |||
}; | |||
[MemberData(nameof(InvalidValues))] | |||
public void MyMethod_InvalidValue_Error(string reference, int id) | |||
{} | |||
</filebox> | </filebox> | ||
Version du 19 février 2020 à 13:34
Convention de nommage
Projet de test | MyProject.Tests |
Classe | MyClassTests |
Méthode | 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
MyProject.Tests/MyClassTests |
public static class MyClassTests { private MyClass myObj; public MyClassTests() { myObj = new MyClass(); } [Fact] public void MyMethod_EmptyString_Zero() { Assert.Equal(myObj.MyMethod(""), 0); } [Fact] public void MyMethod_NullString_ThrowArgumentException() { Assert.Throws<ArgumentException>(() => myObj.MyMethod(null)); } [Theory] [InlineData("-1")] [InlineData("1")] [InlineData("9.8765")] public void MyMethod_NumberAsString_DecimalDifferentFromZero(string value) { Assert.NotEqual(myObj.MyMethod(value), 0); } } |
Theory Data
MyProject.Tests/MyClassTests |
[Theory] [InlineData("-1")] [InlineData("1")] [InlineData("9.8765")] public void MyMethod_NumberAsString_DecimalDifferentFromZero(string value) { Assert.NotEqual(myObj.MyMethod(value), 0); } public static IEnumerable<object[]> InvalidValues => new List<object[]> { new object[] { "AAA", 111 }, new object[] { "AAA", 111 } }; [MemberData(nameof(InvalidValues))] public void MyMethod_InvalidValue_Error(string reference, int id) {} |
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 md MyProject.Tests cd MyProject.Tests dotnet new xunit # 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. |
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"); |
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(); |
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(); |