Comment dois-je tester le code threadé unitaire?


704

Jusqu'à présent, j'ai évité le cauchemar qui teste le code multi-thread car il semble tout simplement trop d'un champ de mines. Je voudrais savoir comment les gens ont testé le code qui s'appuie sur les threads pour une exécution réussie, ou comment les gens ont testé les types de problèmes qui n'apparaissent que lorsque deux threads interagissent d'une manière donnée?

Cela semble être un problème vraiment clé pour les programmeurs aujourd'hui, il serait utile de mettre en commun nos connaissances sur celui-ci à mon humble avis.


2
Je pensais publier une question sur ce même problème. Bien que Will fasse bon nombre de bons points ci-dessous, je pense que nous pouvons faire mieux. Je suis d'accord qu'il n'y a pas d '"approche" unique pour traiter cela proprement. Cependant, «tester le mieux possible» place la barre très bas. Je reviendrai avec mes découvertes.
Zach Burlingame, le

En Java: le package java.util.concurrent contient des classes mal connues, qui peuvent aider à écrire des tests JUnit déterministes. Jetez un oeil à - CountDownLatch - Sémaphore - Échangeur
Synox

Pouvez-vous fournir un lien vers votre précédente question relative aux tests unitaires, s'il vous plaît?
Andrew Grimm


7
Je pense qu'il est important de noter que cette question a 8 ans, et les bibliothèques d'applications ont parcouru un long chemin entre-temps. À «l'ère moderne» (2016), le développement multithread survient principalement dans les systèmes embarqués. Mais si vous travaillez sur une application de bureau ou de téléphone, explorez d'abord les alternatives. Les environnements d'application comme .NET incluent désormais des outils pour gérer ou simplifier considérablement 90% des scénarios multi-threads courants. (asnync / wait, PLinq, IObservable, le TPL ...). Le code multithread est difficile. Si vous ne réinventez pas la roue, vous n'avez pas à la retester.
Paul Williams

Réponses:


245

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 gérer le test d'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 qui a plusieurs threads agissant dessus et que vous ne pouvez pas tester des situations où ces threads se chevauchent, votre conception doit être refaite. C'est à la fois aussi simple et aussi complexe que cela.

Il existe de nombreuses façons de programmer le multithreading qui évite que les threads ne 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 les emplacements de votre conception où les threads interagissent avec la même instance et réduire le nombre de ces emplacements. En procédant ainsi, vous isolez quelques classes où le multithreading se produit réellement, ce qui réduit la complexité globale des tests 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 threads se marchent. 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 peut vous obliger à coder spécifiquement pour les tests, et c'est au mieux un demi-pas vers une vraie solution.

La meilleure façon de tester le code pour les problèmes de thread est probablement par l'analyse statique du code. Si votre code threadé ne suit pas un ensemble fini de modèles thread-safe, 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, dans l'état actuel des choses (et cela durera probablement encore un bon moment), la meilleure façon de tester des applications multithread est de réduire autant que possible la complexité du code threadé. Minimisez les zones où les threads interagissent, testez le mieux possible et utilisez l'analyse de code pour identifier les zones dangereuses.


1
L'analyse de code est excellente si vous traitez avec un langage / framework qui le permet. EG: Findbugs trouvera des problèmes de concurrence partagée très simples et faciles avec des variables statiques. Ce qu'il ne peut pas trouver, c'est des modèles de conception singleton, il suppose que tous les objets peuvent être créés plusieurs fois. Ce plugin est terriblement inadéquat pour les frameworks comme Spring.
Zombies

3
il y a en fait un remède: les objets actifs. drdobbs.com/parallel/prefer-using-active-objects-instead-of-n/…
Dill

6
Bien que ce soit un bon conseil, je me demande toujours: "comment puis-je tester ces zones minimales où plusieurs threads sont requis?"
Bryan Rayner

5
"Si c'est trop complexe à tester, vous le faites mal" - nous devons tous plonger dans du code hérité que nous n'avons pas écrit. Comment cette observation aide-t-elle exactement quelqu'un?
Ronna

2
L'analyse statique est probablement une bonne idée, mais ce n'est pas un test. Ce message ne répond vraiment pas à la question, qui porte sur la façon de tester.
Warren Dew

96

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

La réponse de kleolb02 est bonne. J'essaierai 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 pouvoir programmer des tests reproductibles , ce qui est le plus grand défi du code multithread. Ma réponse vise donc à forcer le code asynchrone dans un faisceau de test, qui fonctionne de manière synchrone .

C'est une idée du livre de Gerard Meszardos " xUnit Test Patterns " et s'appelle "Humble Object" (p. 695): Vous devez séparer le code logique de base et tout ce qui sent le code asynchrone les uns des autres. Cela entraînerait une classe pour la logique de base, qui fonctionne de manière synchrone .

Cela vous met en position de tester le code logique de base de manière synchrone . Vous avez un contrôle absolu sur le timing des appels que vous effectuez sur la logique principale et pouvez ainsi effectuer des tests reproductibles . Et c'est votre avantage de séparer la logique principale et la logique asynchrone.

Cette logique de base doit être enveloppée par une autre classe, qui est chargée de recevoir les appels à la logique de base de manière asynchrone et délègue ces appels à la logique de base. Le code de production n'accédera à la logique principale que via cette classe. Parce que cette classe ne devrait déléguer que les appels, c'est une classe très "stupide" sans beaucoup de logique. Ainsi, vous pouvez garder vos tests unitaires pour cette classe ouvrière asychrone au minimum.

Tout ce qui est supérieur à cela (tester l'interaction entre les classes) sont des tests de composants. Dans ce cas également, vous devriez avoir un contrôle absolu sur le timing, si vous vous en tenez au modèle "Humble Object".


1
Mais parfois, si les threads coopèrent bien entre eux, quelque chose devrait également être testé, non? Certainement, je séparerai la logique principale de la partie asynchrone après avoir lu votre réponse. Mais je vais toujours tester la logique via des interfaces asynchrones avec un rappel de travail sur tous les threads.
CopperCash

Qu'en est-il des systèmes multiprocesseurs?
Technophile

65

Un dur en effet! Dans mes tests unitaires (C ++), j'ai divisé cela en plusieurs catégories selon le modèle de concurrence utilisé:

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

  2. Tests unitaires pour les objets Monitor (ceux qui exécutent des méthodes synchronisées dans le thread de contrôle des appelants) qui exposent une API publique synchronisée - instanciez plusieurs threads factices qui exercent l'API. Construisez des scénarios qui exercent les conditions internes de l'objet passif. Incluez un test de fonctionnement plus long qui en vaut la peine à 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 les objets actifs (ceux qui encapsulent leur propre thread ou threads de contrôle) - similaires au n ° 2 ci-dessus avec des variations selon 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 dans les files d'attente ou doivent être retirées de la file d'attente. Il existe de nombreuses combinaisons possibles ici; boîte blanche loin. Nécessite toujours plusieurs threads simulés pour effectuer des appels à l'objet testé.

En aparté:

Dans la formation de développeur interne que je fais, j'enseigne les piliers de la concurrence et ces deux modèles comme cadre principal de réflexion et de décomposition des 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. Cela conduit également à un code plus testable à l'unité, comme décrit ci-dessus.


51

J'ai rencontré ce problème plusieurs fois ces dernières années lors de l'écriture de code de gestion de threads pour plusieurs projets. Je fournis une réponse tardive car la plupart des autres réponses, tout en fournissant des alternatives, ne répondent pas réellement à 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 être complet, mais je discute également des tests unitaires.

Écriture de code multithread testable

La première chose à faire est de séparer le code de gestion de votre thread de production de tout le code qui effectue le traitement des données. De cette façon, le traitement des données peut être testé en tant que 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 en 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 puis à le déboguer jusqu'à ce qu'il fonctionne est une mauvaise idée pour le code multithread; il en résultera du code où les bogues faciles sont corrigés et les bogues 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 bogues en premier lieu. Si vous avez correctement supprimé le code de traitement des données, le code de gestion des threads doit être suffisamment petit - de préférence quelques lignes, au pire quelques dizaines de lignes - pour que vous puissiez l'écrire sans écrire de bogue, et certainement sans écrire de nombreux bogues. , si vous comprenez le filetage, prenez votre temps et faites attention.

Écriture de tests unitaires pour du code multithread

Une fois que le code multithread est écrit aussi soigneusement que possible, il vaut toujours la peine d'écrire des tests pour ce code. Le but principal des tests n'est pas tant de tester des bogues de conditions de course très dépendants du temps - 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 bogues permet à plusieurs threads d'interagir comme prévu .

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

Si les retards sont trop courts, le test devient fragile, car des différences de synchronisation mineures - disons entre les différentes machines sur lesquelles les tests peuvent être exécutés - peuvent entraîner une interruption du chronométrage et un échec du test. Ce que j'ai généralement fait, c'est commencer avec des retards qui provoquent des échecs de test, augmenter les retards pour que le test passe de manière fiable sur ma machine de développement, puis doubler les retards au-delà de cela pour que le test ait de bonnes chances de passer sur d'autres machines. Cela signifie que le test prendra un temps macroscopique, bien que d'après mon expérience, une conception de test minutieuse peut limiter ce temps à pas plus d'une douzaine de secondes. Comme vous ne devriez pas avoir beaucoup d'endroits nécessitant du code de coordination de threads 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%, on peut s'attendre à ce qu'il détecte environ 80% de vos bogues. Si votre test est bien conçu mais ne trouve aucun bogue, il y a une chance raisonnable que vous n'ayez pas de bogues supplémentaires qui n'apparaîtront qu'en production. Si le test détecte un ou deux bogues, 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 gestion des threads, car il est probable que le code contienne encore des bogues cachés qui seront très difficiles à trouver jusqu'à ce que le code soit en production, et très difficile à réparer alors.


3
Les tests ne peuvent révéler que la présence de bugs, pas leur absence. La question d'origine concerne un problème à 2 fils, auquel cas des tests exhaustifs peuvent être possibles, mais ce n'est souvent pas le cas. Pour tout ce qui dépasse les scénarios les plus simples, vous devrez peut-être mordre la balle et utiliser des méthodes formelles - mais ne sautez pas les tests unitaires! L'écriture d'un code multithread correct est difficile en premier lieu, mais un problème tout aussi difficile le protège du futur contre la régression.
Paul Williams

4
Résumé étonnant de l'un des moyens les moins bien compris. Votre réponse est de frapper la véritable ségrégation que les gens oublient généralement.
prash

1
Une dizaine de secondes, c'est assez long, même si vous n'avez que quelques centaines de tests de cette durée ...
Toby Speight

1
@TobySpeight Les tests sont longs par rapport aux tests unitaires normaux. J'ai trouvé qu'une demi-douzaine de tests sont plus que suffisants si le code threadé est correctement conçu pour être aussi simple que possible, cependant - avoir besoin de quelques centaines de tests multithreads indiquerait certainement un arrangement de threading trop complexe.
Warren Dew

2
C'est un bon argument pour garder votre logique de thread aussi séparable que possible de la fonctionnalité (je sais, beaucoup plus facile à dire qu'à faire). Et, si possible, diviser la suite de tests en ensembles "à chaque changement" et "pré-validé" (afin que vos tests minute par minute ne soient pas trop impactés).
Toby Speight

22

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

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


20

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

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

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

ConTest d'IBM Research est intéressant. Il instrumente votre code en insérant toutes sortes de comportements de modification de threads (par exemple sommeil et rendement) pour essayer de découvrir des bogues de manière aléatoire.

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

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


16

L'attente peut également être utile pour vous aider à écrire des tests unitaires déterministes. Il vous permet d'attendre la mise à jour d'un état quelque part dans votre système. Par exemple:

await().untilCall( to(myService).myMethod(), greaterThan(3) );

ou

await().atMost(5,SECONDS).until(fieldIn(myObject).ofType(int.class), equalTo(1));

Il prend également en charge Scala et Groovy.

await until { something() > 4 } // Scala example

1
L'attente est géniale - exactement ce que je cherchais!
Forge_7

14

Une autre façon de (un peu) tester du code threadé, et des systèmes très complexes en général, est par le biais de Fuzz Testing . Ce n'est pas génial, et il ne trouvera pas tout, mais il est susceptible d'être utile et simple à faire.

Citation:

Le test Fuzz ou fuzzing est une technique de test logiciel qui fournit des données aléatoires ("fuzz") aux entrées d'un programme. Si le programme échoue (par exemple, en se bloquant ou en échouant les assertions de code intégrées), les défauts peuvent être notés. Le grand avantage du test Fuzz est que la conception du test est extrêmement simple et exempte d'idées préconçues sur le comportement du système.

...

Les tests Fuzz sont souvent utilisés dans les grands projets de développement de logiciels qui utilisent des tests de boîte noire. Ces projets ont généralement un budget pour développer des outils de test, et le test fuzz est l'une des techniques qui offre un rapport avantages / coûts élevé.

...

Cependant, le test fuzz ne remplace pas un test exhaustif ou des méthodes formelles: il ne peut fournir qu'un échantillon aléatoire du comportement du système, et dans de nombreux cas, réussir un test fuzz peut seulement démontrer qu'un logiciel gère les exceptions sans se bloquer, plutôt que se comporter correctement. Ainsi, le test fuzz ne peut être considéré que comme un outil de recherche de bogues plutôt que comme une assurance de qualité.


13

J'en ai fait beaucoup, et oui ça craint.

Quelques conseils:

  • GroboUtils pour exécuter plusieurs threads de test
  • alphaWorks ConTest aux classes d'instruments pour faire varier les entrelacements entre les itérations
  • Créez un throwablechamp et archivez-le tearDown(voir Listing 1). Si vous interceptez une mauvaise exception dans un autre thread, affectez-la simplement à throwable.
  • J'ai créé la classe utils dans le Listing 2 et je l'ai trouvée précieuse, en particulier waitForVerify et waitForCondition, ce qui augmentera considérablement les performances de vos tests.
  • Faites bon usage de AtomicBooleanvos tests. Il est thread-safe et vous aurez souvent besoin d'un type de référence final 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 d'attente à votre test (par exemple, @Test(timeout=60*1000)), car les tests de simultanéité peuvent parfois se bloquer indéfiniment lorsqu'ils sont interrompus.

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());
}

2
Un délai d'attente est une bonne idée, mais si un test arrive à expiration, tout résultat ultérieur de cette exécution est suspect. Le test de temporisation peut encore avoir des threads en cours d'exécution qui peuvent vous gâcher.
Don Kirkby

12

Tester le code MT pour l'exactitude est, comme déjà indiqué, un problème assez difficile. En fin de compte, cela revient à s'assurer qu'il n'y a pas de courses de données mal synchronisées dans votre code. Le problème avec cela est qu'il existe une infinité de possibilités d'exécution de threads (entrelacements) sur lesquelles vous n'avez pas beaucoup de contrôle (assurez-vous de lire ceci article, cependant). Dans des scénarios simples, il pourrait être possible de prouver réellement l'exactitude par le raisonnement, mais ce n'est généralement pas le cas. Surtout si vous voulez éviter / minimiser la synchronisation et ne pas opter pour l'option de synchronisation la plus évidente / la plus simple.

Une approche que je suis consiste à écrire du code de test hautement simultané afin de faire en sorte que des courses de données potentiellement non détectées se produisent. Et puis je lance ces tests pendant un certain temps :) Une fois, je suis tombé sur une conversation où un informaticien montrait un outil de ce genre (en créant au hasard un test à partir des spécifications et en les exécutant de manière sauvage, simultanément, en vérifiant les invariants définis) être cassé).

Soit dit en passant, je pense que cet aspect du test du code MT n'a pas été mentionné ici: identifier les invariants du code que vous pouvez vérifier au hasard. Malheureusement, trouver ces invariants est également un problème assez difficile. De plus, ils peuvent ne pas tenir tout le temps pendant l'exécution, vous devez donc trouver / appliquer des points d'exécution où vous pouvez vous attendre à ce qu'ils soient vrais. Apporter l'exécution du code à un tel état est également un problème difficile (et pourrait lui-même entraîner des problèmes de concurrence. Ouf, c'est sacrément difficile!

Quelques liens intéressants à lire:


L'auteur fait référence à la randomisation dans les tests. Il peut s'agir de QuickCheck , qui a été porté dans de nombreuses langues. Vous pouvez regarder la discussion sur ces tests pour le système simultané ici
Max

6

Pete Goodliffe a une série sur les tests unitaires de code threadé .

C'est dur. Je prends la voie la plus facile et j'essaie de garder le code de threading abstrait du test réel. Pete mentionne que ma façon de procéder est erronée, mais j'ai la bonne séparation ou j'ai juste eu de la chance.


6
J'ai lu les deux articles publiés jusqu'à présent et je ne les ai pas trouvés très utiles. Il parle juste des difficultés sans donner beaucoup de conseils concrets. Peut-être que les futurs articles s'amélioreront.
Don Kirkby

6

Pour Java, consultez le chapitre 12 de JCIP . Il existe des exemples concrets d'écriture de tests unitaires déterministes et multithreads pour au moins tester l'exactitude et les invariants du code simultané.

"Prouver" la sécurité des threads avec des tests unitaires est beaucoup plus difficile. Ma conviction est que cela est mieux servi par des tests d'intégration automatisés sur une variété de plates-formes / configurations.


6

J'aime écrire deux ou plusieurs méthodes de test à exécuter sur des threads parallèles, et chacune d'elles fait des appels dans l'objet testé. J'ai utilisé des appels Sleep () pour coordonner l'ordre des appels des différents threads, mais ce n'est pas vraiment fiable. C'est aussi beaucoup plus lent car vous devez dormir suffisamment longtemps pour que le timing fonctionne généralement.

J'ai trouvé la bibliothèque Java TC multithread du même groupe qui a écrit FindBugs. Il vous permet de spécifier l'ordre des événements sans utiliser Sleep (), et il est fiable. Je ne l'ai pas encore essayé.

La plus grande limitation de cette approche est qu'elle ne vous permet que de tester les scénarios que vous suspectez de causer des problèmes. Comme d'autres l'ont dit, vous devez vraiment isoler votre code multithread dans un petit nombre de classes simples pour avoir tout espoir de les tester en profondeur.

Une fois que vous avez soigneusement testé les scénarios que vous prévoyez de causer des problèmes, un test non scientifique qui lance un tas de demandes simultanées à la classe pendant un certain temps est un bon moyen de rechercher des problèmes inattendus.

Mise à jour: j'ai joué un peu avec la bibliothèque Java TC multithread, et cela fonctionne bien. J'ai également porté certaines de ses fonctionnalités vers une version .NET que j'appelle TickingTest .


5

Je gère les tests unitaires des composants filetés de la même manière que je gère tout test unitaire, c'est-à-dire avec inversion des cadres de contrôle et d'isolement. Je développe dans l'arène .Net et hors de la boîte, le filetage (entre autres) est très difficile (je dirais presque impossible) à isoler complètement.

Par conséquent, j'ai écrit des wrappers qui ressemblent à ceci (simplifié):

public interface IThread
{
    void Start();
    ...
}

public class ThreadWrapper : IThread
{
    private readonly Thread _thread;

    public ThreadWrapper(ThreadStart threadStart)
    {
        _thread = new Thread(threadStart);
    }

    public Start()
    {
        _thread.Start();
    }
}

public interface IThreadingManager
{
    IThread CreateThread(ThreadStart threadStart);
}

public class ThreadingManager : IThreadingManager
{
    public IThread CreateThread(ThreadStart threadStart)
    {
         return new ThreadWrapper(threadStart)
    }
}

À partir de là, je peux facilement injecter IThreadingManager dans mes composants et utiliser le cadre d'isolation de mon choix pour que le thread se comporte comme je m'y attendais pendant le test.

Jusqu'à présent, cela a très bien fonctionné pour moi, et j'utilise la même approche pour le pool de threads, les choses dans System.Environment, Sleep, etc., etc.


5

Jetez un oeil à ma réponse connexe à

Conception d'une classe de test pour une barrière personnalisée

Il est biaisé vers Java mais a un résumé raisonnable des options.

En résumé, (IMO) ce n'est pas l'utilisation d'un cadre sophistiqué qui garantira l'exactitude, mais comment vous allez concevoir votre code multithread. La division des préoccupations (concurrence et fonctionnalité) contribue grandement à accroître la confiance. La croissance d'un logiciel orienté objet guidé par des tests explique certaines options mieux que moi.

L'analyse statique et les méthodes formelles (voir Concurrence: modèles d'état et programmes Java ) sont une option mais je les ai trouvées d'une utilité limitée dans le développement commercial.

N'oubliez pas que les tests de style de chargement / trempage sont rarement garantis pour mettre en évidence des problèmes.

Bonne chance!


Vous devez également mentionner votre tempus-fugitbibliothèque ici, qui helps write and test concurrent code;)
Idolon

4

J'ai récemment découvert (pour Java) un outil appelé Threadsafe. C'est un outil d'analyse statique un peu comme findbugs, mais spécifiquement pour repérer les problèmes de multi-threading. Ce n'est pas un remplacement pour les tests mais je peux le recommander dans le cadre de l'écriture de Java multi-thread fiable.

Il détecte même des problèmes potentiels très subtils autour de choses comme la subsomption de classe, l'accès à des objets dangereux via des classes simultanées et la détection de modificateurs volatils manquants lors de l'utilisation du paradigme de verrouillage à double vérification.

Si vous écrivez Java multithread, essayez-le .


3

L'article suivant propose 2 solutions. Envelopper un sémaphore (CountDownLatch) et ajoute des fonctionnalités comme externaliser les données du thread interne. Un autre moyen d'atteindre cet objectif consiste à utiliser Thread Pool (voir Points d'intérêt).

Sprinkler - Objet de synchronisation avancée


3
Veuillez expliquer les approches ici, les liens externes pourraient être morts à l'avenir.
Uooo

2

J'ai passé la majeure partie de la semaine dernière dans une bibliothèque universitaire à étudier le débogage de code simultané. Le problème central est que le code concurrent n'est pas déterministe. En règle générale, le débogage académique est tombé dans l'un des trois camps ici:

  1. Suivi d'événement / relecture. Cela nécessite un moniteur d'événements, puis un examen des événements qui ont été envoyés. Dans un cadre UT, cela impliquerait d'envoyer manuellement les événements dans le cadre d'un test, puis d'effectuer des examens post-mortem.
  2. Scriptable. C'est là que vous interagissez avec le code en cours d'exécution avec un ensemble de déclencheurs. "Sur x> foo, baz ()". Cela pourrait être interprété dans un cadre UT où vous avez un système d'exécution déclenchant un test donné dans une certaine condition.
  3. Interactif. Cela ne fonctionnera évidemment pas dans une situation de test automatique. ;)

Maintenant, comme l'ont remarqué les commentateurs ci-dessus, vous pouvez concevoir votre système simultané dans un état plus déterministe. Cependant, si vous ne le faites pas correctement, vous revenez à la conception d'un système séquentiel.

Ma suggestion serait de se concentrer sur un protocole de conception très strict sur ce qui est enfilé et ce qui ne l'est pas. Si vous contraignez votre interface pour qu'il y ait un minimum de dépendances entre les éléments, c'est beaucoup plus facile.

Bonne chance et continuez à travailler sur le problème.


2

J'ai eu la malheureuse tâche de tester du code threadé et ce sont certainement les tests les plus difficiles que j'ai jamais écrits.

Lors de la rédaction de mes tests, j'ai utilisé une combinaison de délégués et d'événements. Fondamentalement, il s'agit d'utiliser des PropertyNotifyChangedévénements avec un WaitCallbackou une sorte de ConditionalWaitersondages.

Je ne sais pas si c'était la meilleure approche, mais cela a fonctionné pour moi.


1

En supposant que sous le code "multi-thread", cela signifiait quelque chose qui est

  • avec état et mutable
  • ET accessible / modifié par plusieurs threads simultanément

En d'autres termes, nous parlons de tester une classe / méthode / unité thread-safe personnalisée avec état - qui devrait être une bête très rare de nos jours.

Parce que cette bête est rare, nous devons d'abord nous assurer qu'il existe toutes les excuses valables pour l'écrire.

Étape 1. Envisagez de modifier l'état dans le même contexte de synchronisation.

Aujourd'hui, il est facile d'écrire du code simultané et asynchrone à composer où les E / S ou autres opérations lentes sont déchargées en arrière-plan mais l'état partagé est mis à jour et interrogé dans un contexte de synchronisation. par exemple les tâches asynchrones / en attente et Rx dans .NET, etc. - elles sont toutes testables par conception, les "vraies" tâches et les planificateurs peuvent être substitués pour rendre les tests déterministes (mais cela est hors de portée de la question).

Cela peut sembler très contraint, mais cette approche fonctionne étonnamment bien. Il est possible d'écrire des applications entières dans ce style sans avoir besoin de rendre tout état thread-safe (je le fais).

Étape 2. Si la manipulation de l'état partagé sur un contexte de synchronisation unique n'est absolument pas possible.

Assurez-vous que la roue n'est pas réinventée / il n'y a certainement aucune alternative standard qui peut être adaptée pour le travail. Il devrait être probable que le code soit très cohésif et contenu dans une seule unité, par exemple avec de bonnes chances, c'est un cas particulier d'une structure de données standard sécurisée pour les threads comme une carte de hachage ou une collection ou autre.

Remarque: si le code est volumineux / s'étend sur plusieurs classes ET a besoin d'une manipulation d'état multi-thread, il y a de fortes chances que la conception ne soit pas bonne, reconsidérez l'étape 1

Étape 3. Si cette étape est atteinte, nous devons tester notre propre classe / méthode / unité thread-safe avec état .

Je serai très honnête: je n'ai jamais eu à écrire de tests appropriés pour un tel code. La plupart du temps, je m'éloigne à l'étape 1, parfois à l'étape 2. La dernière fois que j'ai dû écrire du code thread-safe personnalisé, c'était il y a tellement d'années que c'était avant d'adopter les tests unitaires / probablement je n'aurais pas à l'écrire avec les connaissances actuelles de toute façon.

Si je devais vraiment tester un tel code ( enfin, la réponse réelle ), j'essaierais deux ou trois choses ci-dessous

  1. Tests de résistance non déterministes. Par exemple, exécutez 100 threads simultanément et vérifiez que le résultat final est cohérent. Ceci est plus typique pour les tests de niveau supérieur / d'intégration de plusieurs scénarios d'utilisateurs, mais peut également être utilisé au niveau de l'unité.

  2. Exposez des «hooks» de test où test peut injecter du code pour aider à créer des scénarios déterministes où un thread doit effectuer l'opération avant l'autre. Aussi laid soit-il, je ne vois rien de mieux.

  3. Des tests retardés pour faire fonctionner les threads et effectuer des opérations dans un ordre particulier. À strictement parler, ces tests ne sont pas non plus déterministes (il existe un risque de gel du système / arrêt de la collection GC du monde qui peut fausser les retards autrement orchestrés), il est également moche mais permet d'éviter les crochets.


0

Pour le code J2E, j'ai utilisé SilkPerformer, LoadRunner et JMeter pour les tests de concurrence des threads. Ils font tous la même chose. Fondamentalement, ils vous offrent une interface relativement simple pour administrer leur version du serveur proxy, nécessaire, afin d'analyser le flux de données TCP / IP et de simuler plusieurs utilisateurs effectuant des demandes simultanées à votre serveur d'applications. Le serveur proxy peut vous donner la possibilité d'analyser les demandes, en présentant la page entière et l'URL envoyées au serveur, ainsi que la réponse du serveur, après avoir traité la demande.

Vous pouvez trouver des bogues en mode http non sécurisé, où vous pouvez au moins analyser les données du formulaire qui sont envoyées et les modifier systématiquement pour chaque utilisateur. Mais les vrais tests sont lorsque vous exécutez dans https (Secured Socket Layers). Ensuite, vous devez également composer avec une modification systématique des données de session et de cookie, ce qui peut être un peu plus compliqué.

Le meilleur bogue que j'ai jamais trouvé, lors du test de la concurrence, a été lorsque j'ai découvert que le développeur s'était appuyé sur la récupération de place Java pour fermer la demande de connexion établie lors de la connexion au serveur LDAP lors de la connexion. Cela a entraîné l'exposition des utilisateurs aux sessions des autres utilisateurs et aux résultats très déroutants, lorsque vous essayez d'analyser ce qui s'est passé lorsque le serveur a été mis à genoux, à peine capable de terminer une transaction, toutes les quelques secondes.

En fin de compte, vous ou quelqu'un devrez probablement boucler et analyser le code pour les erreurs comme celui que je viens de mentionner. Et une discussion ouverte entre les départements, comme celle qui s'est produite, lorsque nous avons dévoilé le problème décrit ci-dessus, est très utile. Mais ces outils sont la meilleure solution pour tester du code multi-thread. JMeter est open source. SilkPerformer et LoadRunner sont propriétaires. Si vous voulez vraiment savoir si votre application est sécurisée pour les threads, c'est comme ça que les grands garçons le font. J'ai fait cela pour de très grandes entreprises de façon professionnelle, donc je ne pense pas. Je parle d'expérience personnelle.

Une mise en garde: il faut du temps pour comprendre ces outils. Il ne s'agira pas simplement d'installer le logiciel et de lancer l'interface graphique, sauf si vous avez déjà été exposé à une programmation multi-thread. J'ai essayé d'identifier les 3 catégories critiques de domaines à comprendre (formulaires, données de session et cookies), dans l'espoir qu'au moins commencer par comprendre ces sujets vous aidera à vous concentrer sur des résultats rapides, au lieu d'avoir à lire le documentation complète.


0

La concurrence est une interaction complexe entre le modèle de mémoire, le matériel, les caches et notre code. Dans le cas de Java, au moins ces tests ont été en partie traités principalement par jcstress . Les créateurs de cette bibliothèque sont connus pour être les auteurs de nombreuses fonctionnalités de concurrence JVM, GC et Java.

Mais même cette bibliothèque a besoin d'une bonne connaissance de la spécification Java Memory Model pour que nous sachions exactement ce que nous testons. Mais je pense que cet effort se concentre sur les mircobenchmarks. Pas d'énormes applications commerciales.


0

Il y a un article sur le sujet, utilisant Rust comme langue dans l'exemple de code:

https://medium.com/@polyglot_factotum/rust-concurrency-five-easy-pieces-871f1c62906a

En résumé, l'astuce consiste à écrire votre logique concurrente afin qu'elle soit robuste au non-déterminisme impliqué avec plusieurs threads d'exécution, en utilisant des outils comme les canaux et les condvars.

Ensuite, si c'est ainsi que vous avez structuré vos "composants", la façon la plus simple de les tester est d'utiliser des canaux pour leur envoyer des messages, puis de bloquer sur d'autres canaux pour affirmer que le composant envoie certains messages attendus.

L'article lié est entièrement écrit à l'aide de tests unitaires.


-1

Si vous testez un nouveau thread simple (exécutable) .run () Vous pouvez vous moquer de Thread pour exécuter séquentiellement l'exécutable.

Par exemple, si le code de l'objet testé appelle un nouveau thread comme celui-ci

Class TestedClass {
    public void doAsychOp() {
       new Thread(new myRunnable()).start();
    }
}

Ensuite, se moquer de nouveaux threads et exécuter séquentiellement l'argument exécutable peut aider

@Mock
private Thread threadMock;

@Test
public void myTest() throws Exception {
    PowerMockito.mockStatic(Thread.class);
    //when new thread is created execute runnable immediately 
    PowerMockito.whenNew(Thread.class).withAnyArguments().then(new Answer<Thread>() {
        @Override
        public Thread answer(InvocationOnMock invocation) throws Throwable {
            // immediately run the runnable
            Runnable runnable = invocation.getArgumentAt(0, Runnable.class);
            if(runnable != null) {
                runnable.run();
            }
            return threadMock;//return a mock so Thread.start() will do nothing         
        }
    }); 
    TestedClass testcls = new TestedClass()
    testcls.doAsychOp(); //will invoke myRunnable.run in current thread
    //.... check expected 
}

-3

(si possible) n'utilisez pas de threads, utilisez des acteurs / objets actifs. Facile à tester.


2
@OMTheEternity peut-être mais c'est toujours la meilleure réponse imo.
Dill

-5

Vous pouvez utiliser EasyMock.makeThreadSafe pour rendre le threadsafe d'instance de test


Ce n'est pas du tout un moyen possible de tester du code multithread. Le problème n'est pas que le code de test s'exécute sur plusieurs threads mais que vous testez du code qui s'exécute généralement sur plusieurs threads. Et vous ne pouvez pas tout synchroniser, car vous ne testez plus les courses de données.
bennidi
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.