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
MyProject.Tests/MyClassTests.cs
|
public static class MyClassTests
{
private MyClass myObj;
public MyClassTests()
{
myObj = new MyClass();
}
[Fact]
public void MyMethod_EmptyString_Zero()
{
Assert.Equal(0, myObj.MyMethod(""));
}
[Fact]
public void MyMethod_NullString_ThrowArgumentException()
{
Assert.Throws<ArgumentException>(() => myObj.MyMethod(null));
}
[Fact]
public async Task MyMethodAsync_NullString_ThrowArgumentException()
{
await Assert.ThrowsAsync<ArgumentException>(() => myObj.MyMethodAsync(null));
}
|
 |
Naming: Method name Input Expected output |
 |
Steps:
- Arrange
- Act
- Assert
|
Assert
|
Assert.Empty(myList);
Assert.Single(myList);
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));
Assert.DoesNotContain(myValue, myList);
|
Theory Data
|
[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 }
};
[Theory]
[MemberData(nameof(InvalidValues))]
public void MyMethod_InvalidValue_Error(string reference, int id)
{}
private static readonly MyClass BaseObject = new MyClass
{
Prop1 = 1,
Prop2 = "2"
};
private static MyClass CreateObject(Action<MyClass> update)
{
var object = new MyClass
{
Prop1 = BaseObject.Prop1,
Prop2 = BaseObject.Prop2
};
update(object);
return object;
}
public static IEnumerable<object[]> Objects
=> new List<object[]>
{
new object[]
{
CreateObject(x => x.Prop1++)
},
new object[]
{
CreateConfiguration(x => x.Prop2 += "X")
}
};
|
|
md MaSolution
cd MaSolution
dotnet new sln
md MyProject
cd MyProject
dotnet new console
cd ..
dotnet sln add MyProject/MyProject.csproj
md MyProject.Tests
cd MyProject.Tests
dotnet new xunit
dotnet add reference ../MyProject/MyProject.csproj
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. |
Compare recursively (on 10 levels) the properties of 2 objects.
|
myObject.Should().BeEquivalentTo(myOtherObject);
myObject.Should().BeEquivalentTo(myOtherObject, options =>
options.ExcludingNestedObjects());
|
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.
|
var mockDataService = new Mock<IDataService>();
IDataService dataService = mockDataService.Object;
var myObj = new MyClass(dataService);
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()
{
this.ds.GetPersons();
}
}
public class DataService : IDataService
{
List<Person> GetPersons() { }
}
|
AssemblyInfo.cs
|
[assembly: InternalsVisibleTo("DynamicProxyGenAssembly2")]
|
Methods
|
var persons = new List<Person> { };
mockDataService.Setup(ds => ds.GetPersons())
.Returns(() => persons);
var person = new Person { };
mockDataService.Setup(ds => ds.GetPersonById(It.Is<int>(i => i >= 0))
.Returns(() => person);
mockDataService.Setup(ds => ds.GetPersonById(It.Is<int>(i => i < 0)))
.Throws<ArgumentException>();
mockDataService.Setup(ds => ds.GetPersonById(It.Is<int>()))
.Returns((int id) => personList[i]);
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");
|
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);
|
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. |
|
public string PropertyToMock { get; }
fakeService.SetupGet(x => x.PropertyToMock).Returns("ExpectedValue");
|
Events
|
mockDataService.Raise(m => m.NotifyManager += null, new MyCustomEventArgs("message"));
mockDataService.Raise(m => m.NotifyManager += null, "message", true);
|
Verify
Permet de savoir si une méthode ou une propriété a été appelée.
|
mockDataService.Verify(ds => ds.GetPersons(), "Custom error message");
mockDataService.Verify(m => m.GetPersonById(
It.Is<int>(fn => fn.Equals(1))));
mockDataService.Verify(ds => ds.GetPersons(), Times.Exactly(2));
mockDataService.Verify(ds => ds.GetPersons(), Times.Never);
mockDataService.VerifySet(m => m.Persons = It.IsAny<IList<Person>>());
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.
|
|
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.
|
var mockDataService = new Mock<IDataService>() { DefaultValue = DefaultValue.Mock };
IMyResult result = mockDataService.Object.MyMethod(It.IsAny<string>());
Mock<IMyResult> mockResult = Mock.Get(result);
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 };
var mockDataService1 = mockFactory.Create<IDataService1>();
var mockDataService2 = mockFactory.Create<IDataService2>();
mockFactory.Verify();
|
Membres protected
Permet de mocker un membre protected.
|
using Moq.Protected;
var mockDataService = new Mock<IDataService>();
mockDataService.Protected()
.Setup<string>("MyProtectedMethod", ItExpr.IsAny<string>, ItExpr.IsAny<int>)
.Returns("1234")
.Verifiable();
mockDataService.Verify();
|
 |
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