Comment utiliser JUnit pour tester des processus asynchrones


204

Comment testez-vous les méthodes qui déclenchent des processus asynchrones avec JUnit?

Je ne sais pas comment faire attendre mon test jusqu'à la fin du processus (ce n'est pas exactement un test unitaire, c'est plutôt un test d'intégration car il implique plusieurs classes et pas une seule).


Vous pouvez essayer JAT (Java Asynchronous Test): bitbucket.org/csolar/jat
cs0lar

2
JAT a 1 observateur et n'a pas été mis à jour depuis 1,5 ans. Awaitility a été mis à jour il y a seulement 1 mois et est sur la version 1.6 au moment d'écrire ces lignes. Je ne suis affilié à aucun de ces projets, mais si j'allais investir dans un ajout à mon projet, je donnerais plus de crédit à Awaitility en ce moment.
Les Hazlewood

JAT n'a toujours pas de mises à jour: "Dernière mise à jour le 2013-01-19". Il vous suffit de gagner du temps pour suivre le lien.
Deamon

@LesHazlewood, un observateur est mauvais pour JAT, mais à peu près aucune mise à jour depuis des années ... Juste un exemple. À quelle fréquence mettez-vous à jour la pile TCP de bas niveau de votre système d'exploitation, si cela fonctionne? Alternative à JAT est répondue ci-dessous stackoverflow.com/questions/631598/… .
user1742529

Réponses:


46

À mon humble avis, il est mauvais d'avoir des tests unitaires créer ou attendre sur les threads, etc. Vous souhaitez que ces tests s'exécutent en quelques secondes. C'est pourquoi j'aimerais proposer une approche en 2 étapes pour tester les processus asynchrones.

  1. Testez que votre processus asynchrone est soumis correctement. Vous pouvez vous moquer de l'objet qui accepte vos demandes asynchrones et vous assurer que le travail soumis a des propriétés correctes, etc.
  2. Testez que vos rappels asynchrones font les bonnes choses. Ici, vous pouvez simuler le travail soumis à l'origine et supposer qu'il est correctement initialisé et vérifier que vos rappels sont corrects.

148
Sûr. Mais parfois, vous devez tester du code spécifiquement destiné à gérer les threads.
laquais

77
Pour ceux d'entre nous qui utilisent Junit ou TestNG pour faire des tests d'intégration (et pas seulement des tests unitaires), ou des tests d'acceptation des utilisateurs (par exemple avec Cucumber), attendre une fin asynchrone et vérifier le résultat est absolument nécessaire.
Les Hazlewood

38
Les processus asynchrones sont parmi les codes les plus compliqués à obtenir et vous dites que vous ne devriez pas utiliser de tests unitaires pour eux et ne tester qu'avec un seul thread? C'est une très mauvaise idée.
Charles

18
Les faux tests échouent souvent à prouver que les fonctionnalités fonctionnent de bout en bout. La fonctionnalité asynchrone doit être testée de manière asynchrone pour s'assurer qu'elle fonctionne. Appelez-le un test d'intégration si vous préférez, mais c'est un test qui est toujours nécessaire.
Scott Boring

4
Cela ne devrait pas être la réponse acceptée. Les tests vont au-delà des tests unitaires. L'OP l'appelle plus comme un test d'intégration qu'un test unitaire.
Jeremiah Adams

191

Une alternative consiste à utiliser la classe CountDownLatch .

public class DatabaseTest {

    /**
     * Data limit
     */
    private static final int DATA_LIMIT = 5;

    /**
     * Countdown latch
     */
    private CountDownLatch lock = new CountDownLatch(1);

    /**
     * Received data
     */
    private List<Data> receiveddata;

    @Test
    public void testDataRetrieval() throws Exception {
        Database db = new MockDatabaseImpl();
        db.getData(DATA_LIMIT, new DataCallback() {
            @Override
            public void onSuccess(List<Data> data) {
                receiveddata = data;
                lock.countDown();
            }
        });

        lock.await(2000, TimeUnit.MILLISECONDS);

        assertNotNull(receiveddata);
        assertEquals(DATA_LIMIT, receiveddata.size());
    }
}

REMARQUE, vous ne pouvez pas simplement utiliser la synchronisation avec un objet normal comme verrou, car les rappels rapides peuvent libérer le verrou avant l'appel de la méthode d'attente du verrou. Voir cet article de blog de Joe Walnes.

EDIT Suppression des blocs synchronisés autour de CountDownLatch grâce aux commentaires de @jtahlborn et @Ring


8
veuillez ne pas suivre cet exemple, il est incorrect. vous ne devez pas synchroniser sur un CountDownLatch car il gère la sécurité des threads en interne.
jtahlborn

1
C'était un bon conseil jusqu'à la partie synchronisée, qui consommait probablement près de 3-4 heures de temps de débogage. stackoverflow.com/questions/11007551/…
Anneau

2
Toutes mes excuses pour l'erreur. J'ai correctement édité la réponse.
Martin

7
Si vous vérifiez que onSuccess a été appelé, vous devez affirmer que lock.await renvoie true.
Gilbert

1
@Martin, ce serait correct, mais cela signifierait que vous avez un problème différent qui doit être résolu.
George Aristy

76

Vous pouvez essayer d'utiliser la bibliothèque Awaitility . Cela facilite le test des systèmes dont vous parlez.


21
Un avertissement amical: Johan est le principal contributeur au projet.
dbm

1
Souffre du problème fondamental de devoir attendre (les tests unitaires doivent s'exécuter rapidement ). Idéalement, vous ne voulez vraiment pas attendre une milliseconde de plus que nécessaire, donc je pense que l'utilisation CountDownLatch(voir la réponse de @Martin) est meilleure à cet égard.
George Aristy

Vraiment génial.
R. Karlus

C'est la bibliothèque parfaite qui remplit mes exigences de test d'intégration de processus asynchrone. Vraiment génial. La bibliothèque semble être bien entretenue et possède des fonctionnalités allant de basique à avancé qui, je crois, sont suffisantes pour répondre à la plupart des scénarios. Merci pour la référence géniale!
Tanvir

Suggestion vraiment géniale. Merci
RoyalTiger

68

Si vous utilisez un CompletableFuture (introduit dans Java 8) ou un SettableFuture (de Google Guava ), vous pouvez terminer votre test dès qu'il est terminé, plutôt que d'attendre une durée prédéfinie. Votre test ressemblerait à ceci:

CompletableFuture<String> future = new CompletableFuture<>();
executorService.submit(new Runnable() {         
    @Override
    public void run() {
        future.complete("Hello World!");                
    }
});
assertEquals("Hello World!", future.get());

4
... et si vous êtes coincé avec moins de huit java, essayez les goyaves SettableFuture qui fait à peu près la même chose
Markus T


18

Une méthode que j'ai trouvée assez utile pour tester des méthodes asynchrones consiste à injecter une Executorinstance dans le constructeur de l'objet à tester. En production, l'instance de l'exécuteur est configurée pour s'exécuter de manière asynchrone tandis qu'en test, elle peut être simulée pour s'exécuter de manière synchrone.

Supposons donc que j'essaie de tester la méthode asynchrone Foo#doAsync(Callback c),

class Foo {
  private final Executor executor;
  public Foo(Executor executor) {
    this.executor = executor;
  }

  public void doAsync(Callback c) {
    executor.execute(new Runnable() {
      @Override public void run() {
        // Do stuff here
        c.onComplete(data);
      }
    });
  }
}

En production, je construirais Fooavec une Executors.newSingleThreadExecutor()instance d'Executor tandis qu'en test, je la construirais probablement avec un exécuteur synchrone qui fait ce qui suit -

class SynchronousExecutor implements Executor {
  @Override public void execute(Runnable r) {
    r.run();
  }
}

Maintenant, mon test JUnit de la méthode asynchrone est assez propre -

@Test public void testDoAsync() {
  Executor executor = new SynchronousExecutor();
  Foo objectToTest = new Foo(executor);

  Callback callback = mock(Callback.class);
  objectToTest.doAsync(callback);

  // Verify that Callback#onComplete was called using Mockito.
  verify(callback).onComplete(any(Data.class));

  // Assert that we got back the data that we expected.
  assertEquals(expectedData, callback.getData());
}

Ne fonctionne pas si je veux tester un élément d'intégration qui implique un appel de bibliothèque asynchrone comme celui de SpringWebClient
Stefan Haberl

8

Il n'y a rien de mal à tester le code thread / asynchrone, en particulier si le thread est le point du code que vous testez. L'approche générale pour tester ce genre de choses est de:

  • Bloquer le fil de test principal
  • Capturer les assertions ayant échoué à partir d'autres threads
  • Débloquer le thread de test principal
  • Renversez tous les échecs

Mais c'est beaucoup de passe-partout pour un test. Une approche meilleure / plus simple consiste à simplement utiliser ConcurrentUnit :

  final Waiter waiter = new Waiter();

  new Thread(() -> {
    doSomeWork();
    waiter.assertTrue(true);
    waiter.resume();
  }).start();

  // Wait for resume() to be called
  waiter.await(1000);

L'avantage de cela sur l' CountdownLatchapproche est qu'il est moins détaillé car les échecs d'assertion qui se produisent dans n'importe quel thread sont correctement signalés au thread principal, ce qui signifie que le test échoue quand il le devrait. Un article qui compare l' CountdownLatchapproche de ConcurrentUnit est ici .

J'ai également écrit un blog sur le sujet pour ceux qui veulent en savoir un peu plus.


une solution similaire que j'ai utilisée dans le passé est github.com/MichaelTamm/junit-toolbox , également présentée comme une extension tierce sur junit.org/junit4
dschulten

4

Que diriez-vous d'appeler SomeObject.waitet notifyAllcomme décrit ici OU d'utiliser la méthode Robotiums Solo.waitForCondition(...) OU d'utiliser une classe que j'ai écrite pour le faire (voir les commentaires et la classe de test pour savoir comment utiliser)


1
Le problème avec l'approche wait / notify / interruption est que le code que vous testez peut potentiellement interférer avec les threads en attente (je l'ai vu se produire). C'est pourquoi ConcurrentUnit utilise un circuit privé sur lequel les threads peuvent attendre, qui ne peuvent pas être perturbés par inadvertance par des interruptions du thread de test principal.
Jonathan

3

Je trouve une bibliothèque socket.io pour tester la logique asynchrone. Il semble simple et bref en utilisant LinkedBlockingQueue . Voici un exemple :

    @Test(timeout = TIMEOUT)
public void message() throws URISyntaxException, InterruptedException {
    final BlockingQueue<Object> values = new LinkedBlockingQueue<Object>();

    socket = client();
    socket.on(Socket.EVENT_CONNECT, new Emitter.Listener() {
        @Override
        public void call(Object... objects) {
            socket.send("foo", "bar");
        }
    }).on(Socket.EVENT_MESSAGE, new Emitter.Listener() {
        @Override
        public void call(Object... args) {
            values.offer(args);
        }
    });
    socket.connect();

    assertThat((Object[])values.take(), is(new Object[] {"hello client"}));
    assertThat((Object[])values.take(), is(new Object[] {"foo", "bar"}));
    socket.disconnect();
}

En utilisant LinkedBlockingQueue, prenez l'API pour bloquer jusqu'à obtenir le résultat de la même manière que synchrone. Et définissez un délai pour éviter de supposer trop de temps pour attendre le résultat.


1
Super approche!
aminographie

3

Il convient de mentionner qu'il existe un chapitre très utile Testing Concurrent Programsdans Concurrence en pratique qui décrit certaines approches de test unitaire et donne des solutions aux problèmes.


1
Quelle approche est-ce? Pouvez-vous donner un exemple?
Bruno Ferreira

2

C'est ce que j'utilise de nos jours si le résultat du test est produit de manière asynchrone.

public class TestUtil {

    public static <R> R await(Consumer<CompletableFuture<R>> completer) {
        return await(20, TimeUnit.SECONDS, completer);
    }

    public static <R> R await(int time, TimeUnit unit, Consumer<CompletableFuture<R>> completer) {
        CompletableFuture<R> f = new CompletableFuture<>();
        completer.accept(f);
        try {
            return f.get(time, unit);
        } catch (InterruptedException | TimeoutException e) {
            throw new RuntimeException("Future timed out", e);
        } catch (ExecutionException e) {
            throw new RuntimeException("Future failed", e.getCause());
        }
    }
}

En utilisant les importations statiques, le test se lit plutôt bien. (note, dans cet exemple, je commence un fil pour illustrer l'idée)

    @Test
    public void testAsync() {
        String result = await(f -> {
            new Thread(() -> f.complete("My Result")).start();
        });
        assertEquals("My Result", result);
    }

S'il f.completen'est pas appelé, le test échouera après une temporisation. Vous pouvez également utiliser f.completeExceptionallypour échouer tôt.


2

Il existe de nombreuses réponses ici, mais la plus simple consiste simplement à créer un CompletableFuture terminé et à l'utiliser:

CompletableFuture.completedFuture("donzo")

Donc dans mon test:

this.exactly(2).of(mockEventHubClientWrapper).sendASync(with(any(LinkedList.class)));
this.will(returnValue(new CompletableFuture<>().completedFuture("donzo")));

Je m'assure juste que toutes ces choses sont appelées de toute façon. Cette technique fonctionne si vous utilisez ce code:

CompletableFuture.allOf(calls.toArray(new CompletableFuture[0])).join();

Il se faufilera à travers lui car tous les CompletableFutures sont terminés!


2

Évitez de tester avec des threads parallèles chaque fois que vous le pouvez (ce qui est la plupart du temps). Cela ne fera que rendre vos tests floconneux (parfois réussir, parfois échouer).

Ce n'est que lorsque vous devez appeler une autre bibliothèque / système, que vous devrez peut-être attendre sur d'autres threads, dans ce cas, utilisez toujours la bibliothèque Awaitility à la place de Thread.sleep().

N'appelez jamais simplement get()ou join()dans vos tests, sinon vos tests pourraient s'exécuter indéfiniment sur votre serveur CI au cas où l'avenir ne se terminerait jamais. Affirmez toujours d' isDone()abord dans vos tests avant d'appeler get(). Pour CompletionStage, c'est .toCompletableFuture().isDone().

Lorsque vous testez une méthode non bloquante comme celle-ci:

public static CompletionStage<String> createGreeting(CompletableFuture<String> future) {
    return future.thenApply(result -> "Hello " + result);
}

alors vous ne devez pas simplement tester le résultat en passant un Future terminé dans le test, vous devez également vous assurer que votre méthode doSomething()ne bloque pas en appelant join()ou get(). Ceci est particulièrement important si vous utilisez un framework non bloquant.

Pour ce faire, testez avec un futur non terminé que vous définissez comme terminé manuellement:

@Test
public void testDoSomething() throws Exception {
    CompletableFuture<String> innerFuture = new CompletableFuture<>();
    CompletableFuture<String> futureResult = createGreeting(innerFuture).toCompletableFuture();
    assertFalse(futureResult.isDone());

    // this triggers the future to complete
    innerFuture.complete("world");
    assertTrue(futureResult.isDone());

    // futher asserts about fooResult here
    assertEquals(futureResult.get(), "Hello world");
}

De cette façon, si vous ajoutez future.join()à doSomething (), le test échouera.

Si votre service utilise un ExecutorService tel que dans thenApplyAsync(..., executorService), alors dans vos tests injectez un ExecutorService à un seul thread, tel que celui de goyave:

ExecutorService executorService = Executors.newSingleThreadExecutor();

Si votre code utilise le forkJoinPool tel que thenApplyAsync(...), réécrivez le code pour utiliser un ExecutorService (il existe de nombreuses bonnes raisons) ou utilisez Awaitility.

Pour raccourcir l'exemple, j'ai fait de BarService un argument de méthode implémenté en tant que lambda Java8 dans le test, généralement ce serait une référence injectée dont vous vous moqueriez.


Hé @tkruse, peut-être avez-vous un dépôt public avec un test utilisant cette technique?
Cristiano

@Christiano: ce serait contraire à la philosophie SO. Au lieu de cela, j'ai changé les méthodes pour compiler sans aucun code supplémentaire (toutes les importations sont java8 + ou junit) lorsque vous les collez dans une classe de test junit vide. N'hésitez pas à voter.
tkruse

J'ai compris maintenant. Merci. Mon problème est maintenant de tester quand les méthodes retournent CompletableFuture mais acceptent d'autres objets comme paramètres autres qu'un CompletableFuture.
Cristiano

Dans votre cas, qui crée le CompletableFuture que la méthode renvoie? S'il s'agit d'un autre service, on peut se moquer de moi et ma technique s'applique toujours. Si la méthode elle-même crée un CompletableFuture, la situation change beaucoup, vous pouvez donc poser une nouvelle question à ce sujet. Cela dépend ensuite du thread qui terminera le futur que votre méthode retourne.
tkruse

1

Je préfère utiliser attendre et notifier. C'est simple et clair.

@Test
public void test() throws Throwable {
    final boolean[] asyncExecuted = {false};
    final Throwable[] asyncThrowable= {null};

    // do anything async
    new Thread(new Runnable() {
        @Override
        public void run() {
            try {
                // Put your test here.
                fail(); 
            }
            // lets inform the test thread that there is an error.
            catch (Throwable throwable){
                asyncThrowable[0] = throwable;
            }
            // ensure to release asyncExecuted in case of error.
            finally {
                synchronized (asyncExecuted){
                    asyncExecuted[0] = true;
                    asyncExecuted.notify();
                }
            }
        }
    }).start();

    // Waiting for the test is complete
    synchronized (asyncExecuted){
        while(!asyncExecuted[0]){
            asyncExecuted.wait();
        }
    }

    // get any async error, including exceptions and assertationErrors
    if(asyncThrowable[0] != null){
        throw asyncThrowable[0];
    }
}

Fondamentalement, nous devons créer une référence de tableau finale, à utiliser à l'intérieur d'une classe interne anonyme. Je préfère créer un booléen [], car je peux mettre une valeur à contrôler si nous devons attendre (). Lorsque tout est terminé, nous libérons simplement l'asyncExecuted.


1
Si votre assertion échoue, le thread de test principal ne le saura pas.
Jonathan

Merci pour la solution, m'aide à déboguer le code avec une connexion websocket.
Nazar Sakharenko

@Jonathan, j'ai mis à jour le code pour intercepter toute assertion et exception et l'informer du thread de test principal.
Paulo

1

Pour tous les utilisateurs de Spring, voici comment je fais habituellement mes tests d'intégration de nos jours, où le comportement asynchrone est impliqué:

Déclenchez un événement d'application dans le code de production lorsqu'une tâche asynchrone (telle qu'un appel d'E / S) est terminée. La plupart du temps, cet événement est de toute façon nécessaire pour gérer la réponse de l'opération asynchrone en production.

Avec cet événement en place, vous pouvez ensuite utiliser la stratégie suivante dans votre scénario de test:

  1. Exécuter le système sous test
  2. Écoutez l'événement et assurez-vous que l'événement s'est déclenché
  3. Faites vos affirmations

Pour décomposer cela, vous aurez d'abord besoin d'une sorte d'événement de domaine à déclencher. J'utilise un UUID ici pour identifier la tâche qui s'est terminée, mais vous êtes bien sûr libre d'utiliser quelque chose d'autre tant qu'il est unique.

(Notez que les extraits de code suivants utilisent également des annotations Lombok pour se débarrasser du code de plaque de chaudière)

@RequiredArgsConstructor
class TaskCompletedEvent() {
  private final UUID taskId;
  // add more fields containing the result of the task if required
}

Le code de production lui-même ressemble alors généralement à ceci:

@Component
@RequiredArgsConstructor
class Production {

  private final ApplicationEventPublisher eventPublisher;

  void doSomeTask(UUID taskId) {
    // do something like calling a REST endpoint asynchronously
    eventPublisher.publishEvent(new TaskCompletedEvent(taskId));
  }

}

Je peux ensuite utiliser un ressort @EventListenerpour attraper l'événement publié dans le code de test. L'écouteur d'événements est un peu plus impliqué, car il doit gérer deux cas de manière sécurisée pour les threads:

  1. Le code de production est plus rapide que le scénario de test et l'événement s'est déjà déclenché avant que le scénario de test ne vérifie l'événement, ou
  2. Le scénario de test est plus rapide que le code de production et le scénario de test doit attendre l'événement.

A CountDownLatchest utilisé pour le deuxième cas, comme mentionné dans d'autres réponses ici. Notez également que l' @Orderannotation de la méthode du gestionnaire d'événements garantit que cette méthode du gestionnaire d'événements est appelée après tout autre écouteur d'événements utilisé en production.

@Component
class TaskCompletionEventListener {

  private Map<UUID, CountDownLatch> waitLatches = new ConcurrentHashMap<>();
  private List<UUID> eventsReceived = new ArrayList<>();

  void waitForCompletion(UUID taskId) {
    synchronized (this) {
      if (eventAlreadyReceived(taskId)) {
        return;
      }
      checkNobodyIsWaiting(taskId);
      createLatch(taskId);
    }
    waitForEvent(taskId);
  }

  private void checkNobodyIsWaiting(UUID taskId) {
    if (waitLatches.containsKey(taskId)) {
      throw new IllegalArgumentException("Only one waiting test per task ID supported, but another test is already waiting for " + taskId + " to complete.");
    }
  }

  private boolean eventAlreadyReceived(UUID taskId) {
    return eventsReceived.remove(taskId);
  }

  private void createLatch(UUID taskId) {
    waitLatches.put(taskId, new CountDownLatch(1));
  }

  @SneakyThrows
  private void waitForEvent(UUID taskId) {
    var latch = waitLatches.get(taskId);
    latch.await();
  }

  @EventListener
  @Order
  void eventReceived(TaskCompletedEvent event) {
    var taskId = event.getTaskId();
    synchronized (this) {
      if (isSomebodyWaiting(taskId)) {
        notifyWaitingTest(taskId);
      } else {
        eventsReceived.add(taskId);
      }
    }
  }

  private boolean isSomebodyWaiting(UUID taskId) {
    return waitLatches.containsKey(taskId);
  }

  private void notifyWaitingTest(UUID taskId) {
    var latch = waitLatches.remove(taskId);
    latch.countDown();
  }

}

La dernière étape consiste à exécuter le système testé dans un scénario de test. J'utilise un test SpringBoot avec JUnit 5 ici, mais cela devrait fonctionner de la même manière pour tous les tests utilisant un contexte Spring.

@SpringBootTest
class ProductionIntegrationTest {

  @Autowired
  private Production sut;

  @Autowired
  private TaskCompletionEventListener listener;

  @Test
  void thatTaskCompletesSuccessfully() {
    var taskId = UUID.randomUUID();
    sut.doSomeTask(taskId);
    listener.waitForCompletion(taskId);
    // do some assertions like looking into the DB if value was stored successfully
  }

}

Notez que contrairement à d'autres réponses ici, cette solution fonctionnera également si vous exécutez vos tests en parallèle et que plusieurs threads exercent le code asynchrone en même temps.


0

Si vous voulez tester la logique, ne la testez pas de manière asynchrone.

Par exemple pour tester ce code qui fonctionne sur les résultats d'une méthode asynchrone.

public class Example {
    private Dependency dependency;

    public Example(Dependency dependency) {
        this.dependency = dependency;            
    }

    public CompletableFuture<String> someAsyncMethod(){
        return dependency.asyncMethod()
                .handle((r,ex) -> {
                    if(ex != null) {
                        return "got exception";
                    } else {
                        return r.toString();
                    }
                });
    }
}

public class Dependency {
    public CompletableFuture<Integer> asyncMethod() {
        // do some async stuff       
    }
}

Dans le test, simulez la dépendance avec une implémentation synchrone. Le test unitaire est complètement synchrone et s'exécute en 150 ms.

public class DependencyTest {
    private Example sut;
    private Dependency dependency;

    public void setup() {
        dependency = Mockito.mock(Dependency.class);;
        sut = new Example(dependency);
    }

    @Test public void success() throws InterruptedException, ExecutionException {
        when(dependency.asyncMethod()).thenReturn(CompletableFuture.completedFuture(5));

        // When
        CompletableFuture<String> result = sut.someAsyncMethod();

        // Then
        assertThat(result.isCompletedExceptionally(), is(equalTo(false)));
        String value = result.get();
        assertThat(value, is(equalTo("5")));
    }

    @Test public void failed() throws InterruptedException, ExecutionException {
        // Given
        CompletableFuture<Integer> c = new CompletableFuture<Integer>();
        c.completeExceptionally(new RuntimeException("failed"));
        when(dependency.asyncMethod()).thenReturn(c);

        // When
        CompletableFuture<String> result = sut.someAsyncMethod();

        // Then
        assertThat(result.isCompletedExceptionally(), is(equalTo(false)));
        String value = result.get();
        assertThat(value, is(equalTo("got exception")));
    }
}

Vous ne testez pas le comportement asynchrone mais vous pouvez tester si la logique est correcte.

En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.