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 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);
}
}
|
|
# 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>();
// arguments
It.IsIn(1, 2, 3);
It.IsInRange(1, 9, Range.Inclusive);
It.IsRegex("[1-9]");
|
|
public async Task<string> MethodAsync() { /* ... */ };
fakeService.Setup(x => x.MethodAsync())
.ReturnsAsync("Result");
//.Returns(Task.FromResult("Result"));
|
Sequence
|
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. |
|
// 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 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();
|
NSubstitute