unit-testingDémarrer avec les tests unitaires


Remarques

Les tests unitaires décrivent le processus de test des unités de code individuelles indépendamment du système dont elles font partie. Ce qui constitue une unité peut varier d'un système à l'autre, allant d'une méthode individuelle à un groupe de classes étroitement liées ou d'un module.

L'unité est isolée de ses dépendances à l'aide de tests doubles si nécessaire et configurée dans un état connu. Son comportement en réaction aux stimuli (appels de méthode, événements, données simulées) est ensuite testé par rapport au comportement attendu.

Les tests unitaires de systèmes entiers peuvent être réalisés à l'aide de faisceaux de test écrits personnalisés. Cependant, de nombreux frameworks de test ont été écrits pour simplifier le processus et prendre en charge une grande partie des tâches de plomberie, répétitives et banales. Cela permet aux développeurs de se concentrer sur ce qu'ils veulent tester.

Lorsqu'un projet a suffisamment de tests unitaires, toute modification apportée à l'ajout de nouvelles fonctionnalités ou à la refactorisation de code peut être effectuée facilement en vérifiant à la fin que tout fonctionne comme avant.

La couverture du code , normalement exprimée en pourcentage, est la mesure type utilisée pour indiquer la part du code dans un système couverte par les tests unitaires; notez qu'il n'y a pas de règle stricte quant à la hauteur que cela devrait être, mais il est généralement admis que plus le niveau est élevé, mieux c'est.

Le développement piloté par les tests (TDD) est un principe qui spécifie qu'un développeur doit commencer à coder en écrivant un test unitaire défaillant et ensuite seulement pour écrire le code de production qui fait passer le test. En pratiquant le TDD, on peut dire que les tests eux-mêmes sont le premier consommateur du code en cours de création; Par conséquent, ils aident à auditer et à piloter la conception du code de manière à ce qu'il soit aussi simple à utiliser et aussi robuste que possible.

Versions

Le test unitaire est un concept qui n'a pas de numéro de version.

Un test XUnit avec des paramètres

using Xunit;

public class SimpleCalculatorTests
{
    [Theory]
    [InlineData(0, 0, 0, true)]
    [InlineData(1, 1, 2, true)]
    [InlineData(1, 1, 3, false)]
    public void Add_PassMultipleParameters_VerifyExpected(
        int inputX, int inputY, int expected, bool isExpectedCorrect)
    {
        // Arrange
        var sut = new SimpleCalculator();

        // Act
        var actual = sut.Add(inputX, inputY);

        // Assert
        if (isExpectedCorrect)
        {
            Assert.Equal(expected, actual);
        }
        else
        {
            Assert.NotEqual(expected, actual);
        }
    }
}

public class SimpleCalculator
{
    public int Add(int x, int y)
    {
        return x + y;
    }
}
 

Un test élémentaire de base en python

import unittest

def addition(*args):
    """ add two or more summands and return the sum """

    if len(args) < 2:
        raise ValueError, 'at least two summands are needed'
    
    for ii in args: 
        if not isinstance(ii, (int, long, float, complex )):
            raise TypeError

    # use build in function to do the job
    return sum(args) 
 

Maintenant la partie test:

class Test_SystemUnderTest(unittest.TestCase):

    def test_addition(self):
        """test addition function"""

        # use only one summand - raise an error 
        with self.assertRaisesRegexp(ValueError, 'at least two summands'):
            addition(1)
        
        # use None - raise an error
        with self.assertRaises(TypeError):
            addition(1, None)
        
        # use ints and floats 
        self.assertEqual(addition(1, 1.), 2)

        # use complex numbers
        self.assertEqual(addition(1, 1., 1+2j), 3+2j)

if __name__ == '__main__':
    unittest.main()
 

Un test élémentaire de base

Dans sa forme la plus simple, un test unitaire comprend trois étapes:

  • Préparer l'environnement pour le test
  • Exécutez le code à tester
  • Valider le comportement attendu correspond au comportement observé

Ces trois étapes sont souvent appelées «Arrange-Act-Assert» ou «Given-When-Then».

Vous trouverez ci-dessous un exemple en C # qui utilise le framework NUnit .

[TestFixture]
public CalculatorTest
{
   [Test]
   public void Add_PassSevenAndThree_ExpectTen()
   {
       // Arrange - setup environment
       var systemUnderTest = new Calculator();         

       // Act - Call system under test
       var calculatedSum = systemUnderTest.Add(7, 3);  
       
       // Assert - Validate expected result
       Assert.AreEqual(10, calculatedSum);             
  }
}
 

Si nécessaire, une quatrième étape de nettoyage facultative est prévue.

Un test unitaire avec un espion (test d'interaction)

Test de l'unité classique teste l' état , mais il peut être impossible de tester correctement les méthodes dont le comportement dépend d'autres classes via l'état. Nous testons ces méthodes à l'aide de tests d'interaction , qui vérifient que le système testé appelle correctement ses collaborateurs. Étant donné que les collaborateurs ont leurs propres tests unitaires, cela est suffisant et constitue en fait un meilleur test de la responsabilité réelle de la méthode testée. Nous ne testons pas que cette méthode retourne un résultat donné à une entrée, mais appelle plutôt ses collaborateurs.

// Test that squareOfDouble invokes square() with the doubled value

let systemUnderTest = new Calculator()          // Arrange - setup environment
let square = spy()
systemUnderTest.setSquare(square)               //   inject a spy

let actual = systemUnderTest.squareOfDouble(3)  // Act - Call system under test

assert(square.calledWith(6))                    // Assert - Validate expected interaction
 

Un test unitaire avec une dépendance par écrasement

Les bons tests unitaires sont indépendants, mais le code a souvent des dépendances. Nous utilisons différents types de doubles de test pour supprimer les dépendances à tester. L'un des tests les plus simples est un stub. Ceci est une fonction avec une valeur de retour codée en dur appelée à la place de la dépendance du monde réel.

// Test that oneDayFromNow returns a value 24*60*60 seconds later than current time

let systemUnderTest = new FortuneTeller()       // Arrange - setup environment
systemUnderTest.setNow(() => {return 10000})    //   inject a stub which will 
                                                //   return 10000 as the result

let actual = systemUnderTest.oneDayFromNow()    // Act - Call system under test

assert.equals(actual, 10000 + 24 * 60 * 60)     // Assert - Validate expected result
 

Dans le code de production, oneDayFromNow appellerait Date.now (), mais cela provoquerait des tests incohérents et peu fiables. Donc, ici, nous le supprimons.

Test simple Java + JUnit

JUnit est la principale infrastructure de test utilisée pour tester le code Java.

La classe testée modélise un compte bancaire simple, qui facture une pénalité lorsque vous êtes à découvert.

public class BankAccount {
    private int balance;

    public BankAccount(int i){
        balance = i;
    }

    public BankAccount(){
        balance = 0;
    }

    public int getBalance(){
        return balance;
    }

    public void deposit(int i){
        balance += i;
    }

    public void withdraw(int i){
        balance -= i;
        if (balance < 0){
            balance -= 10; // penalty if overdrawn
        }
    }
}
 

Cette classe de test valide le comportement de certaines méthodes publiques BankAccount .

import org.junit.Test;
import static org.junit.Assert.*;

// Class that tests
public class BankAccountTest{

    BankAccount acc;

    @Before                        // This will run **before** EACH @Test
    public void setUptestDepositUpdatesBalance(){
        acc = new BankAccount(100);  
    } 

    @After                        // This Will run **after** EACH @Test
    public void tearDown(){
    // clean up code
    }

    @Test
    public void testDeposit(){
       // no need to instantiate a new BankAccount(), @Before does it for us

        acc.deposit(100);

        assertEquals(acc.getBalance(),200); 
    }

    @Test
    public void testWithdrawUpdatesBalance(){    
        acc.withdraw(30);

        assertEquals(acc.getBalance(),70); // pass
    }

    @Test
    public void testWithdrawAppliesPenaltyWhenOverdrawn(){

        acc.withdraw(120);

        assertEquals(acc.getBalance(),-30);
    }
}
 

Test unitaire avec paramètres utilisant NUnit et C #

using NUnit.Framework;

namespace MyModuleTests 
{
    [TestFixture]
    public class MyClassTests
    {
        [TestCase(1, "Hello", true)]
        [TestCase(2, "bye", false)]
        public void MyMethod_WhenCalledWithParameters_ReturnsExpected(int param1, string param2, bool expected)
        {
        //Arrange
        var foo = new MyClass(param1);

        //Act
        var result = foo.MyMethod(param2);

        //Assert
        Assert.AreEqual(expected, result);
        }
    }
}