Question Comment dois-je tester l'unité de code fileté?


J'ai jusqu'ici évité le cauchemar qui teste le code multithreadé car il semble juste trop de champ de mines. Je voudrais demander comment les gens ont testé le code qui repose sur des threads pour une exécution réussie, ou simplement comment les gens ont testé ces types de problèmes qui n'apparaissent que lorsque deux threads interagissent d'une certaine manière.

Cela semble être un problème très important pour les programmeurs aujourd'hui, il serait utile de mettre en commun nos connaissances sur celui-ci.


584
2017-08-15 11:44


origine


Réponses:


Regardez, il n'y a pas de moyen facile de le faire. Je travaille sur un projet qui est intrinsèquement multithread. Les événements proviennent du système d'exploitation et je dois les traiter simultanément.

La façon la plus simple de tester un code d'application complexe et multithread est la suivante: Si c'est trop complexe à tester, vous le faites mal. Si vous avez une seule instance sur laquelle plusieurs threads agissent, et que vous ne pouvez pas tester les situations où ces threads se chevauchent, alors votre design doit être refait. C'est à la fois aussi simple et complexe que ça.

Il y a plusieurs façons de programmer pour le multithreading qui évite les threads qui traversent les instances en même temps. Le plus simple est de rendre tous vos objets immuables. Bien sûr, ce n'est généralement pas possible. Vous devez donc identifier ces endroits dans votre conception où les threads interagissent avec la même instance et réduire le nombre de ces espaces. En faisant cela, vous isolez quelques classes où le multithreading se produit réellement, ce qui réduit la complexité globale du test de votre système.

Mais vous devez réaliser que même en faisant cela, vous ne pouvez toujours pas tester chaque situation où deux fils se chevauchent. Pour ce faire, vous devez exécuter deux threads simultanément dans le même test, puis contrôler exactement les lignes qu'ils exécutent à un moment donné. Le mieux que vous puissiez faire est de simuler cette situation. Mais cela pourrait vous obliger à coder spécifiquement pour les tests, et c'est au mieux un demi-pas vers une véritable solution.

La meilleure façon de tester le code pour les problèmes de threading est probablement l'analyse statique du code. Si votre code threadé ne suit pas un ensemble fini de modèles de thread sûr, vous pouvez avoir un problème. Je crois que l'analyse de code dans VS contient une certaine connaissance du filetage, mais probablement pas beaucoup.

Regardez, en l'état actuel des choses (et probablement restera un bon moment à venir), la meilleure façon de tester des applications multithread est de réduire la complexité du code threadé autant que possible. Minimisez les zones où les threads interagissent, testez le mieux possible et utilisez l'analyse de code pour identifier les zones dangereuses.


215
2017-08-15 13:29



Cela fait un moment que cette question a été postée, mais elle n'a toujours pas de réponse ...

kleolb02La réponse est bonne. Je vais essayer d'entrer dans plus de détails.

Il y a un moyen, que je pratique pour le code C #. Pour les tests unitaires, vous devriez être capable de programmer reproductible tests, qui est le plus grand défi dans le code multithread. Donc, ma réponse vise à forcer le code asynchrone dans un harnais de test, qui fonctionne synchrone.

C'est une idée du livre de Gérard Meszardos "Modèles de test xUnit"et s'appelle" Humble Object "(p.699): Vous devez séparer le code logique de base et tout ce qui sent le code asynchrone l'un de l'autre, ce qui se traduirait par une classe pour la logique de base, qui fonctionne synchrone.

Cela vous met dans la position de tester le code logique de base dans un synchrone façon. Vous avez un contrôle absolu sur le calendrier des appels que vous faites sur la logique de base et peut donc faire reproductible tests. Et c'est votre gain de séparer la logique de base et la logique asynchrone.

Cette logique de base doit être enveloppée par une autre classe, qui est responsable de la réception des appels à la logique de base de manière asynchrone et délégués ces appels à la logique de base. Le code de production accédera uniquement à la logique principale via cette classe. Parce que cette classe devrait seulement déléguer des appels, c'est une classe très "bête" sans beaucoup de logique. Ainsi, vous pouvez conserver au minimum vos tests unitaires pour cette classe de travail asynchrone.

Tout ce qui précède (tester l'interaction entre les classes) sont des tests de composants. Dans ce cas également, vous devriez être en mesure d'avoir un contrôle absolu sur la synchronisation, si vous respectez le modèle "Humble Object".


79
2018-01-18 12:30



Tough one en effet! Dans mes tests unitaires (C ++), je l'ai décomposé en plusieurs catégories selon le modèle de concurrence utilisé:

  1. Les tests unitaires pour les classes qui fonctionnent dans un seul thread et ne sont pas sensibles au thread - facile, testez comme d'habitude.

  2. Tests unitaires pour Surveiller les objets (ceux qui exécutent des méthodes synchronisées dans le thread de contrôle des appelants) qui exposent une API publique synchronisée - instancient plusieurs threads fictifs qui exercent l'API. Construire des scénarios qui exercent les conditions internes de l'objet passif. Inclure un test de fonctionnement plus long qui bat fondamentalement le diable à partir de plusieurs threads pendant une longue période de temps. C'est non scientifique, je sais, mais cela renforce la confiance.

  3. Tests unitaires pour Objets actifs (ceux qui encapsulent leur propre thread ou threads de contrôle) - similaire à # 2 ci-dessus avec des variations en fonction de la conception de la classe. L'API publique peut être bloquante ou non bloquante, les appelants peuvent obtenir des contrats à terme, les données peuvent arriver à des files d'attente ou doivent être retirées. Il y a beaucoup de combinaisons possibles ici; boîte blanche loin. Encore nécessite plusieurs fils fictifs pour effectuer des appels à l'objet testé.

En aparté:

En formation de développeur interne que je fais, j'enseigne le Piliers de la concurrence et ces deux modèles comme le cadre principal pour penser et décomposer les problèmes de concurrence. Il y a évidemment des concepts plus avancés, mais j'ai trouvé que cet ensemble de bases aide à garder les ingénieurs hors de la soupe. Il conduit également à un code qui est plus unité testable, comme décrit ci-dessus.


55
2017-08-15 13:26



J'ai fait face à ce problème plusieurs fois au cours des dernières années lors de l'écriture du code de gestion de thread pour plusieurs projets. Je fournis une réponse tardive parce que la plupart des autres réponses, tout en fournissant des solutions de rechange, ne répondent pas vraiment à la question sur les tests. Ma réponse s'adresse aux cas où il n'y a pas d'alternative au code multithread; Je couvre les problèmes de conception de code pour l'exhaustivité, mais je discute aussi des tests unitaires.

Ecriture de code multithreadable testable

La première chose à faire est de séparer votre code de gestion des threads de production de tout le code qui traite les données. De cette façon, le traitement des données peut être testé sous la forme d'un code à thread unique, et la seule chose que fait le code multithread est de coordonner les threads.

La deuxième chose à retenir est que les bogues dans le code multithread sont probabilistes; les bogues qui se manifestent le moins fréquemment sont les bogues qui se faufileront dans la production, seront difficiles à reproduire même en production, et causeront ainsi les plus gros problèmes. Pour cette raison, l'approche de codage standard consistant à écrire le code rapidement et à le déboguer jusqu'à ce que cela fonctionne est une mauvaise idée pour le code multithread; il en résultera un code où les bugs faciles sont corrigés et les bugs dangereux sont toujours là.

Au lieu de cela, lors de l'écriture de code multithread, vous devez écrire le code avec l'attitude que vous allez éviter d'écrire les bugs en premier lieu. Si vous avez correctement supprimé le code de traitement des données, le code de gestion des threads doit être assez petit - de préférence quelques lignes, au pire quelques dizaines de lignes - que vous avez la possibilité de l'écrire sans écrire de bogue , si vous comprenez le filetage, prenez votre temps et faites attention.

Ecriture de tests unitaires pour le code multithread

Une fois que le code multithread est écrit aussi soigneusement que possible, il est toujours intéressant d'écrire des tests pour ce code. Le but principal des tests n'est pas tant de tester les conditions de course dépendantes du timing - il est impossible de tester de telles conditions de course de manière répétée - mais plutôt de tester que votre stratégie de verrouillage pour empêcher de tels bugs permet d'interagir comme prévu .

Pour tester correctement le comportement de verrouillage correct, un test doit démarrer plusieurs threads. Pour rendre le test répétable, nous voulons que les interactions entre les threads se produisent dans un ordre prévisible. Nous ne souhaitons pas synchroniser de manière externe les threads dans le test, car cela masquera les bogues qui pourraient se produire en production, lorsque les threads ne sont pas synchronisés de manière externe. Cela laisse l'utilisation de retards de synchronisation pour la synchronisation de threads, qui est la technique que j'ai utilisée avec succès chaque fois que j'ai eu à écrire des tests de code multithread.

Si les retards sont trop courts, le test devient fragile, car des différences mineures de synchronisation - disons entre différentes machines sur lesquelles les tests peuvent être exécutés - peuvent entraîner l'arrêt du chronométrage et l'échec du test. En général, je commence par des retards qui provoquent des échecs de test, augmentent les retards pour que le test passe de manière fiable sur mon ordinateur de développement, puis doublons les retards pour que le test ait de bonnes chances de transmettre d'autres machines. Cela signifie que le test prendra un temps macroscopique, bien que dans mon expérience, une conception de test minutieuse peut limiter ce temps à pas plus d'une douzaine de secondes. Puisque vous ne devriez pas avoir beaucoup d'endroits nécessitant du code de coordination de thread dans votre application, cela devrait être acceptable pour votre suite de tests.

Enfin, gardez une trace du nombre de bugs détectés par votre test. Si votre test a une couverture de code de 80%, il peut s'attendre à attraper environ 80% de vos bugs. Si votre test est bien conçu mais ne trouve aucun bogue, il y a une chance raisonnable que vous n'ayez pas d'autres bugs qui apparaîtront seulement en production. Si le test attrape un ou deux bugs, vous pourriez toujours avoir de la chance. Au-delà de cela, et vous voudrez peut-être envisager un examen attentif ou même une réécriture complète de votre code de traitement de thread, car il est probable que le code contient encore des bogues cachés qui seront très difficiles à trouver jusqu'à ce que le code soit en production. difficile à réparer alors.


32
2017-09-11 21:00



J'ai également eu de sérieux problèmes pour tester le code multithread. Puis j'ai trouvé une solution vraiment cool dans "xUnit Test Patterns" de Gerard Meszaros. Le motif qu'il décrit est appelé Humble objet.

Fondamentalement, il décrit comment vous pouvez extraire la logique dans un composant distinct, facile à tester qui est découplé de son environnement. Après avoir testé cette logique, vous pouvez tester le comportement compliqué (multi-threading, exécution asynchrone, etc ...)


20
2017-08-20 12:29



Il y a quelques outils qui sont assez bons. Voici un résumé de certains des Java.

Certains bons outils d'analyse statique comprennent FindBugs (donne quelques conseils utiles), JLint, Java Pathfinder (JPF et JPF2), et Bogor.

MultithreadedTC est un bon outil d'analyse dynamique (intégré dans JUnit) où vous devez configurer vos propres cas de test.

Concours d'IBM Research est intéressant. Il code votre code en insérant toutes sortes de comportements de modification de thread (par exemple, sleep & yield) pour essayer de découvrir des bogues de manière aléatoire.

TOURNER est un outil vraiment sympa pour modéliser vos composants Java (et autres), mais vous devez avoir un framework utile. Il est difficile à utiliser tel quel, mais extrêmement puissant si vous savez comment l'utiliser. Beaucoup d'outils utilisent SPIN sous le capot.

MultithreadedTC est probablement le plus courant, mais certains des outils d'analyse statique énumérés ci-dessus valent certainement la peine d'être examinés.


16
2017-07-08 12:23



J'ai fait beaucoup de ceci, et oui c'est nul.

Quelques conseils:

  • GroboUtils pour exécuter plusieurs threads de test
  • AlphaWorks ConTest à des classes d'instruments pour faire varier les entrelacements entre les itérations
  • Créer un throwable champ et vérifiez-le tearDown (voir Listing 1). Si vous interceptez une mauvaise exception dans un autre thread, attribuez-le à throwable.
  • J'ai créé la classe utils dans le Listing 2 et je l'ai trouvé inestimable, en particulier waitForVerify et waitForCondition, ce qui augmentera considérablement la performance de vos tests.
  • Faire bon usage de AtomicBoolean dans tes tests. Il est thread-safe, et vous aurez souvent besoin d'un type de référence finale pour stocker les valeurs des classes de rappel et autres. Voir l'exemple dans le Listing 3.
  • Assurez-vous de toujours donner un délai à votre test (par exemple, @Test(timeout=60*1000)), car les tests de simultanéité peuvent parfois être suspendus à jamais lorsqu'ils sont cassés

Listing 1:

@After
public void tearDown() {
    if ( throwable != null )
        throw throwable;
}

Listing 2:

import static org.junit.Assert.fail;
import java.io.File;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;
import java.util.Random;
import org.apache.commons.collections.Closure;
import org.apache.commons.collections.Predicate;
import org.apache.commons.lang.time.StopWatch;
import org.easymock.EasyMock;
import org.easymock.classextension.internal.ClassExtensionHelper;
import static org.easymock.classextension.EasyMock.*;

import ca.digitalrapids.io.DRFileUtils;

/**
 * Various utilities for testing
 */
public abstract class DRTestUtils
{
    static private Random random = new Random();

/** Calls {@link #waitForCondition(Integer, Integer, Predicate, String)} with
 * default max wait and check period values.
 */
static public void waitForCondition(Predicate predicate, String errorMessage) 
    throws Throwable
{
    waitForCondition(null, null, predicate, errorMessage);
}

/** Blocks until a condition is true, throwing an {@link AssertionError} if
 * it does not become true during a given max time.
 * @param maxWait_ms max time to wait for true condition. Optional; defaults
 * to 30 * 1000 ms (30 seconds).
 * @param checkPeriod_ms period at which to try the condition. Optional; defaults
 * to 100 ms.
 * @param predicate the condition
 * @param errorMessage message use in the {@link AssertionError}
 * @throws Throwable on {@link AssertionError} or any other exception/error
 */
static public void waitForCondition(Integer maxWait_ms, Integer checkPeriod_ms, 
    Predicate predicate, String errorMessage) throws Throwable 
{
    waitForCondition(maxWait_ms, checkPeriod_ms, predicate, new Closure() {
        public void execute(Object errorMessage)
        {
            fail((String)errorMessage);
        }
    }, errorMessage);
}

/** Blocks until a condition is true, running a closure if
 * it does not become true during a given max time.
 * @param maxWait_ms max time to wait for true condition. Optional; defaults
 * to 30 * 1000 ms (30 seconds).
 * @param checkPeriod_ms period at which to try the condition. Optional; defaults
 * to 100 ms.
 * @param predicate the condition
 * @param closure closure to run
 * @param argument argument for closure
 * @throws Throwable on {@link AssertionError} or any other exception/error
 */
static public void waitForCondition(Integer maxWait_ms, Integer checkPeriod_ms, 
    Predicate predicate, Closure closure, Object argument) throws Throwable 
{
    if ( maxWait_ms == null )
        maxWait_ms = 30 * 1000;
    if ( checkPeriod_ms == null )
        checkPeriod_ms = 100;
    StopWatch stopWatch = new StopWatch();
    stopWatch.start();
    while ( !predicate.evaluate(null) ) {
        Thread.sleep(checkPeriod_ms);
        if ( stopWatch.getTime() > maxWait_ms ) {
            closure.execute(argument);
        }
    }
}

/** Calls {@link #waitForVerify(Integer, Object)} with <code>null</code>
 * for {@code maxWait_ms}
 */
static public void waitForVerify(Object easyMockProxy)
    throws Throwable
{
    waitForVerify(null, easyMockProxy);
}

/** Repeatedly calls {@link EasyMock#verify(Object[])} until it succeeds, or a
 * max wait time has elapsed.
 * @param maxWait_ms Max wait time. <code>null</code> defaults to 30s.
 * @param easyMockProxy Proxy to call verify on
 * @throws Throwable
 */
static public void waitForVerify(Integer maxWait_ms, Object easyMockProxy)
    throws Throwable
{
    if ( maxWait_ms == null )
        maxWait_ms = 30 * 1000;
    StopWatch stopWatch = new StopWatch();
    stopWatch.start();
    for(;;) {
        try
        {
            verify(easyMockProxy);
            break;
        }
        catch (AssertionError e)
        {
            if ( stopWatch.getTime() > maxWait_ms )
                throw e;
            Thread.sleep(100);
        }
    }
}

/** Returns a path to a directory in the temp dir with the name of the given
 * class. This is useful for temporary test files.
 * @param aClass test class for which to create dir
 * @return the path
 */
static public String getTestDirPathForTestClass(Object object) 
{

    String filename = object instanceof Class ? 
        ((Class)object).getName() :
        object.getClass().getName();
    return DRFileUtils.getTempDir() + File.separator + 
        filename;
}

static public byte[] createRandomByteArray(int bytesLength)
{
    byte[] sourceBytes = new byte[bytesLength];
    random.nextBytes(sourceBytes);
    return sourceBytes;
}

/** Returns <code>true</code> if the given object is an EasyMock mock object 
 */
static public boolean isEasyMockMock(Object object) {
    try {
        InvocationHandler invocationHandler = Proxy
                .getInvocationHandler(object);
        return invocationHandler.getClass().getName().contains("easymock");
    } catch (IllegalArgumentException e) {
        return false;
    }
}
}

Listing 3:

@Test
public void testSomething() {
    final AtomicBoolean called = new AtomicBoolean(false);
    subject.setCallback(new SomeCallback() {
        public void callback(Object arg) {
            // check arg here
            called.set(true);
        }
    });
    subject.run();
    assertTrue(called.get());
}

11
2017-09-24 04:58