Tests unitaires

De Banane Atomic
Aller à la navigationAller à la recherche

Convention de nommage

Assembly naming MyProject.Tests
Unit test class naming MyClassTest
Unit test method naming FeatureBeingTested_StateUnderTest_ExpectedBehavior

Test Driven Development

  1. Transcrire les spécification en tests unitaires
  2. Créer les classes business pour permettre la compilation
  3. À ce stage les tests échouent
  4. Créer la logique business afin que les tests soient validés
  5. Refactoriser le code

AAA Syntaxe

  1. Arrange: initialiser l'élément à tester, créer le mock object et le passer à l'élément à tester
  2. Act: exécuter le test
  3. 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

  1. Ajouter un nouveau projet de type Test → Unit Test Project → MonProjet.Test
  2. Ajouter une référence à MonProjet

Code

Accèder aux éléments privée d'une autre assembly

Csharp.svg
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

  1. Test → Windows → Test Explorer
  2. Run

Debuger les tests

  1. Placer les breakpoint
  2. Clique-droit sur le test → Debug Selected Tests

NUnit

Création d'un projet de tests unitaires

  1. Ajouter un nouveau projet de type NUnit Library Project → MonProjetTest
  2. Ajouter une référence à MonProjet

Code

Csharp.svg
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:
  1. Arrange
  2. Act
  3. Assert

Assert

Cs.svg
// 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

Cs.svg
[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)
{}

Installation pour VSCode et .net core

Bash.svg
# 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.

Cs.svg
// 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.

Cs.svg
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

Cs.svg
// 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

Cs.svg
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.

Cs.svg
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

Cs.svg
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

Cs.svg
// property with a private setter
public string PropertyToMock { get; }

fakeService.SetupGet(x => x.PropertyToMock).Returns("ExpectedValue");

Events

Cs.svg
// 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.

Cs.svg
// 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.
Cs.svg
// 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.

Cs.svg
// 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.

Cs.svg
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.

Cs.svg
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