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




unit-testing (17)

J'ai jusqu'ici évité le cauchemar qui teste le code multithread puisqu'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.


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


Avoir un livre au livre Clean Code CHAPITRE 13 il ya toute une section consacrée à tester le code multithread et aussi à la concurrence en général qui pourrait vous aider à concevoir un meilleur code multithread.


En Java: Le paquet java.util.concurrent contient des classes mal connues, qui peuvent aider à écrire des JUnit-Tests déterministes.

Jettes un coup d'oeil à


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

Certains bons outils d'analyse statique incluent 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.

ConTest d'IBM Research est intéressant. Il code votre code en insérant toutes sortes de comportements modifiant le fil (par exemple, le sommeil et le rendement) pour essayer de découvrir des bogues au hasard.

SPIN 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.


J'ai eu la tâche malheureuse de tester le code fileté et ce sont définitivement les tests les plus difficiles que j'ai jamais écrits.

En écrivant mes tests, j'ai utilisé une combinaison de délégués et d'événements. Fondamentalement, il s'agit d'utiliser les événements PropertyNotifyChanged avec un WaitCallback ou une sorte de ConditionalWaiter qui interroge.

Je ne suis pas sûr si c'était la meilleure approche, mais cela a fonctionné pour moi.


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'instrument pour faire varier les entrelacements entre les itérations
  • Créez un champ throwable et vérifiez-le dans 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.
  • Faites bon usage de AtomicBoolean dans vos 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 une fois qu'ils sont brisé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());
}

J'ai passé la majeure partie de la semaine dernière dans une bibliothèque universitaire à étudier le débogage du code concurrent. Le problème central est que le code concurrent est non-déterministe. Typiquement, le débogage académique est tombé dans l'un des trois camps ici:

  1. Event-trace / rejouer. Cela nécessite un moniteur d'événements, puis l'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 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é sur 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 concurrent 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 n'est pas enfilé. Si vous contraignez votre interface de sorte 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.


J'ai récemment découvert (pour Java) un outil appelé Threadsafe. C'est un outil d'analyse statique, tout comme les 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 capte même des problèmes potentiels très subtils tels que la subsomption de classe, l'accès à des objets dangereux via des classes concurrentes et le repérage de modificateurs volatils manquants lors de l'utilisation du paradigme de verrouillage à double vérification.

Si vous écrivez Java multithread contemplateltd.com/threadsafe


Je gère des tests unitaires de composants filetés de la même manière que n'importe quel test unitaire, c'est-à-dire avec inversion de cadres de contrôle et d'isolation. Je développe dans le .Net-arena et hors de la boîte le filetage (entre autres choses) est très dur (je dirais presque impossible) pour isoler complètement.

J'ai donc é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)
    }
}

De là, je peux facilement injecter le IThreadingManager dans mes composants et utiliser mon cadre d'isolation de choix pour que le thread se comporte comme je l'espère pendant le test.

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


Jetez un oeil à ma réponse connexe à

Concevoir une classe de test pour une barrière personnalisée

C'est biaisé vers Java, mais a un résumé raisonnable des options.

En résumé cependant (OMI) ce n'est pas l'utilisation d'un cadre de fantaisie qui assurera l'exactitude, mais comment vous allez vous concevoir un code multithread. La division des préoccupations (concurrence et fonctionnalité) contribue énormément à accroître la confiance. Logiciel orienté objet orienté par des tests explique certaines options mieux que je peux.

L'analyse statique et les méthodes formelles (voir Concurrency: State Models et Java Programs ) sont une option mais j'ai trouvé qu'elles étaient d'une utilité limitée dans le développement commercial.

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

Bonne chance!


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

"Proving" thread-sécurité 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.


Pour le code J2E, j'ai utilisé SilkPerformer, LoadRunner et JMeter pour tester les threads en simultanéité. Ils font tous la même chose. Fondamentalement, ils vous donnent 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 faisant des demandes simultanées à votre serveur d'applications. Le serveur proxy peut vous donner la possibilité de faire des choses comme analyser les demandes faites, en présentant la page entière et l'URL envoyée au serveur, ainsi que la réponse du serveur, après avoir traité la requête.

Vous pouvez trouver quelques bogues dans le mode http non sécurisé, où vous pouvez au moins analyser les données de formulaire qui sont envoyées, et modifier systématiquement cela pour chaque utilisateur. Mais les vrais tests sont quand vous courez dans https (Sécurisé Socket Layers). Ensuite, vous devez également faire face à la modification systématique de la session et des données de cookie, ce qui peut être un peu plus compliqué.

Le meilleur bug que j'ai pu constater en testant la simultanéité 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 requête de connexion établie lors de la connexion au serveur LDAP lors de la connexion. aux sessions d'autres utilisateurs et des résultats très confus, en essayant d'analyser ce qui s'est passé quand le serveur a été mis à genoux, à peine capable de compléter une transaction, toutes les quelques secondes.

En fin de compte, vous ou quelqu'un devrez probablement s'atteler et analyser le code pour des erreurs comme celle que je viens de mentionner. Et une discussion ouverte entre les départements, comme celle qui a eu lieu, 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 le code multithread. JMeter est open source. SilkPerformer et LoadRunner sont propriétaires. Si vous voulez vraiment savoir si votre application est thread safe, c'est comme ça que les grands garçons le font. Je l'ai fait professionnellement pour de très grandes entreprises, donc je ne devine pas. Je parle d'expérience personnelle.

Un mot d'avertissement: il faut du temps pour comprendre ces outils. Il ne s'agira pas simplement d'installer le logiciel et de lancer l'interface graphique, à moins que vous n'ayez déjà été exposé à la programmation multi-thread. J'ai essayé d'identifier les 3 catégories critiques de zones à comprendre (formulaires, sessions et données de cookies), avec l'espoir qu'au moins commencer par comprendre ces sujets vous aidera à vous concentrer sur des résultats rapides, par opposition à devoir lire le toute la documentation.


Si vous testez un nouveau thread simple (runnable) .run () que vous pouvez simuler Thread pour exécuter le runnable séquentiellement

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

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

Que se moquer des nouveaux threads et exécuter l'argument runnable séquentiellement 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 

Tester l'exactitude du code MT est, comme déjà indiqué, un problème assez difficile. Au final, cela revient à s'assurer qu'il n'y a pas de courses de données incorrectement synchronisées dans votre code. Le problème avec ceci est qu'il y a infiniment de possibilités d'exécution de threads (entrelacements) sur lesquels vous n'avez pas beaucoup de contrôle (assurez-vous de lire this article, cependant). Dans des scénarios simples, il peut être possible de prouver 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 facile.

Une approche que je suis consiste à écrire un code de test hautement concurrent afin de rendre possible des courses de données potentiellement non détectées. Et puis j'exécute ces tests depuis un certain temps :) Une fois je suis tombé sur une discussion où un informaticien montrait un outil qui le faisait (en testant au hasard des spécifications et en les exécutant sauvagement, en vérifiant simultanément les invariants définis être cassé).

En passant, je pense que cet aspect du test de code MT n'a pas été mentionné ici: identifiez les invariants du code que vous pouvez vérifier au hasard. Malheureusement, trouver ces invariants est également un problème assez difficile. En outre, ils ne peuvent 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. Amener l'exécution du code à un tel état est également un problème difficile (et peut même entraîner des problèmes de concurrence.) Oh, c'est sacrément dur!

Quelques liens intéressants à lire:

  • this : Un framework qui permet de forcer certains entrelacements de threads puis de vérifier les invariants
  • jMock Blitzer : synchronisation des tests de stress
  • assertConcurrent : JUnit version de la synchronisation des tests de stress
  • Test de code concurrent : bref aperçu des deux méthodes primaires de la force brute (test de stress) ou déterministe (aller pour les invariants)

Une autre façon de (un peu) tester le code fileté, et les systèmes très complexes en général est à travers Fuzz Testing . Ce n'est pas génial, et il ne trouvera pas tout, mais il sera probablement utile et simple à faire.

Citation:

Fuzz testing ou fuzzing est une technique de test de logiciel qui fournit des données aléatoires ("fuzz") aux entrées d'un programme. Si le programme échoue (par exemple, en plantant ou en échouant dans les assertions de code intégrées), les défauts peuvent être notés. Le grand avantage du test de fuzz est que la conception du test est extrêmement simple et sans préjugés 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 en boîte noire. Ces projets ont généralement un budget pour développer des outils de test, et le test de fuzz est l'une des techniques qui offre un rapport bénéfice / coût élevé.

...

Cependant, les tests fuzz ne remplacent pas les tests exhaustifs ou les méthodes formelles: ils ne peuvent 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. se comporter correctement. Ainsi, le test du fuzz ne peut être considéré que comme un outil de recherche de bug plutôt que comme une garantie de qualité.


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


Pete Goodliffe a une série sur les tests unitaires du code fileté .

C'est dur. Je prends la sortie la plus facile et essaie de garder le code de filetage extrait du test réel. Pete mentionne que la façon dont je le fais est mal, mais j'ai soit la séparation soit je suis juste chanceux.





unit-testing