« Tests unitaires » : différence entre les versions

De Banane Atomic
Aller à la navigationAller à la recherche
Ligne 190 : Ligne 190 :
Extension: [[Visual_studio_code#.NET_Core_Test_Explorer|.NET Core Test Explorer]]
Extension: [[Visual_studio_code#.NET_Core_Test_Explorer|.NET Core Test Explorer]]
{{info | L'utilisation d'un fichier de solution {{boxx|*.sln}} permet de builder tous les projets de la solution en même temps.}}
{{info | L'utilisation d'un fichier de solution {{boxx|*.sln}} permet de builder tous les projets de la solution en même temps.}}
= [https://fluentassertions.com/about/ Fluent assertion] =
== [https://fluentassertions.com/objectgraphs/ Object graph comparison] ==
Compare recursively (on 10 levels) the properties of 2 objects.
<kode lang='cs'>
myObject.Should().BeEquivalentTo(myOtherObject);
// disable recursion
myObject.Should().BeEquivalentTo(myOtherObject, options =>
    options.ExcludingNestedObjects());
</kode>


= MOQ =
= MOQ =

Version du 24 septembre 2020 à 14:31

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.

Fluent assertion

Object graph comparison

Compare recursively (on 10 levels) the properties of 2 objects.

Cs.svg
myObject.Should().BeEquivalentTo(myOtherObject);

// disable recursion
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.

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