Logo Spiria

Comprendre la différence entre un test unitaire et un test d’intégration

4 décembre 2015.

Cet article n’a pas pour objet de fournir une description exhaustive des tests unitaires, mais plutôt d’offrir une compréhension claire de leur rôle et de servir d’avertissement pour les développeurs qui mélangent souvent les tests unitaires et les tests d’intégrations (moi-même, je ne faisais pas exception à mes débuts).

 

Qu’est-ce qu’un test unitaire?

Un test unitaire doit réellement porter sur une seule unité. Pour que ce soit le cas :

  • La classe doit être testé en isolation complète du SUT (système testé) pour assurer que seule la classe et sa méthode est testée.
  • Isoler les variables dépendantes de la classe testée en utilisant un  mécanisme d’injection de dépendances.
  • Utiliser un mocking framework pour injecter des dépendances sous la forme de CAPS.

Puisque les tests unitaires doivent être effectués aussi vite que possible pour fournir un retour quasi immédiat, il doit leur être impossible d’accéder à des fichiers, des bases de données ou tout autre service externe. Les tests qui communiquent avec une base de données ne sont pas des tests unitaires, mais plutôt des tests d’intégration.

Exemple de test unitaire

À titre d’exemple d’un test unitaire où la classe est isolée du reste du SUT, nous utiliserons le mocking framework Rhino.Mock. Un article intéressant sur ses applications se retrouve ici.

D’abord, voici le contrôleur à tester :

using System.Web.Mvc;

namespace MyApp.Controllers
{
    public class EmailController : Controller
    {
        private IEmailService _emailService;

        public EmailController(IEmailService emailService)
        {
            _emailService = emailService;
        }

        /// <summary>
        /// Allows to verify if an email already exists
        /// </summary>
        /// <param name="login"></param>
        /// <returns></returns>
        [HttpPost]
        public ActionResult VerifyEmail(string email)
        {
            if (_emailService.VerifyEmail(email))
                return RedirectToAction("ProceedRegistration");

            return Content("Email already used!");
        }

       public ActionResult ProceedRegistration()
        {
            return View();
        }
    }
}

Deuxièmement, voici un exemplaire de test unitaire :

using Microsoft.VisualStudio.TestTools.UnitTesting;
using Rhino.Mocks;
using MyApp.Services;
using MyApp.Controllers;
using System.Web.Mvc;


namespace TestMyApp
{
    [TestClass]
    public class TestMyApp_Isolation
    {
        //private IEmailService _mockEmailService;
        
        [TestMethod]
        public void TestRecognized()
        {
            IEmailService mockEmailService = MockRepository.GenerateMock<IEmailService>();

            // Arrange
            mockEmailService.Expect(m => m.VerifyEmail(Arg<string>.Is.Anything)).Return(true);
            EmailController emailController = new EmailController(mockEmailService);

            // Act
            var result = emailController.VerifyEmail(Arg<string>.Is.Anything);


            // Assert
            Assert.IsInstanceOfType(result, typeof(RedirectToRouteResult));
            Assert.AreEqual("ProceedRegistration", ((RedirectToRouteResult)result).RouteValues["action"]);
        }

        [TestMethod]
        public void TestNotRecognized()
        {
            IEmailService mockEmailService = MockRepository.GenerateMock<IEmailService>();

            // Arrange
            mockEmailService.Expect(m => m.VerifyEmail(Arg<string>.Is.Anything)).Return(false);
            EmailController emailController = new EmailController(mockEmailService);

            // Act
            var result = emailController.VerifyEmail(Arg<string>.Is.Anything);

            // Assert
            Assert.IsInstanceOfType(result, typeof(ContentResult));
            Assert.AreEqual("Email already used!", ((ContentResult)result).Content);
        }
    }
}

Vous remarquerez que j’ai créé l’objet factice : “IEmailService mockEmailService MockRepository.GenerateMock <IEmailService> ();” j’utilise “Arrange”  pour en fixer le comportement (je m’attends à une réaction spécifique du “EmailService”) “mockEmailService.Expect (m => m.VerifyEmail (Arg <string> .Is.Anything)) Return (true);”

Je suis maintenant capable de vérifier le comportement de ma méthode d’action sur le  contrôleur “VerifyEmail” en cherchant le résultat obtenu après avoir complété l’action “Act” : “var result = emailController.VerifyEmail (Arg <string> .Is.Anything);”

Confronté à une adresse courriel non reconnue, le contrôleur devrait effectuer une redirection vers une autre action pour continuer l’inscription au site web. Je test présentement le contrôleur pour vérifier qu’il s’agit bien d’un redirect et s’il redirige bien au bon contrôleur d'action pour être fonctionnel avec “Assert” :

“Assert.IsInstanceOfType (result, typeof (RedirectToRouteResult));” et “Assert.AreEqual (” ProceedRegistration “((RedirectToRouteResult) result) .RouteValues [” action “]);”

Maintenant, vérifions son comportement lorsqu’une adresse courriel n’est pas reconnue :

1. “Arrange”: “mockEmailService.Expect(m => m.VerifyEmail(Arg<string>.Is.Anything)).Return(false);”

2. “Act”: “var result = emailController.VerifyEmail(Arg<string>.Is.Anything);”

3. “Assert”: “Assert.IsInstanceOfType(result, typeof(ContentResult));” et “Assert.AreEqual(“Email already used!”, ((ContentResult)result).Content);”

Cette fois-ci le résultat attendu est “ContentResult” avec un message d’erreur spécifique : “Email already used!”.

Par cet exemple, nous avons démontré comment tester le comportement d’un contrôleur sans la moindre dépendance. Nous avons réalisé un véritable test unitaire, totalement isolé !

Exemple d’un test d’intégration

Mainteant, jetons un coup d’œil à un test d’intégration, en reprenant l’exemple ci-haut :

Prenons pour acquis que la classe immuable “EmailService” accède à la base de données. 

using Microsoft.VisualStudio.TestTools.UnitTesting;
using Rhino.Mocks;
using MyApp.Services;
using MyApp.Controllers;
using System.Web.Mvc;

namespace TestMyApp
{
    [TestClass]
    public class TestMyApp_Integration
    {
        [TestMethod]
        public void TestRecognized()
        {
            // Arrange
            string email = "recognizedEmail@test.com";
            EmailController emailController = new EmailController(new EmailService());

            // Act
            var result = emailController.VerifyEmail(email);

            // Assert
            Assert.IsInstanceOfType(result, typeof(RedirectToRouteResult));
            Assert.AreEqual("ProceedRegistration", ((RedirectToRouteResult)result).RouteValues["action"]);
        }

        [TestMethod]
        public void TestNotRecognized()
        {
            // Arrange
            string email = "NotRecognizedEmail@test.com";
            EmailController emailController = new EmailController(new EmailService());

            // Act
            var result = emailController.VerifyEmail(email);

            // Assert
            Assert.IsInstanceOfType(result, typeof(ContentResult));
            Assert.AreEqual("Email already used!", ((ContentResult)result).Content);
        }
    }
}

Cet exemple met en évidence les différences entre ce test et le test unitaire : je n’ai pas eu à déterminer de comportement artificiel dans la section “Arrange”. J’ai un véritable paramètre “email” dans une véritable méthode de class, qui réfère à une base de données ! J’ai également vérifié le comportement des contrôleurs à l’aide de dépendances externes, et ce, au cours d’une véritable utilisation de cette fonctionnalité.

J’espère que cet exemple vous a permis de comprendre la différence entre un test unitaire et un test d’intégration. C'est avec plaisir que je répondrai à vos questions dans la section « commentaires ».

Désormais, vous devriez pouvoir différencier un véritable test unitaire d’un test d’intégration.