806 Stimmen

Wie sollte ich Multithreading-Code in der Einheit testen?

Bisher habe ich den Albtraum des Testens von Multithreading-Code vermieden, da er mir einfach zu sehr wie ein Minenfeld erscheint. Ich möchte fragen, wie die Leute über das Testen von Code, der auf Threads für die erfolgreiche Ausführung angewiesen ist, oder einfach nur, wie die Leute über das Testen dieser Art von Fragen, die nur zeigen, wenn zwei Threads in einer bestimmten Weise interagieren gegangen sind?

Dies scheint ein wirklich zentrales Problem für die Programmierer heute, wäre es nützlich, unser Wissen auf dieser einen imho Pool.

2 Stimmen

Ich hatte vor, eine Frage zu genau diesem Thema zu stellen. Obwohl Will im Folgenden viele gute Punkte anführt, denke ich, dass wir es besser machen können. Ich stimme zu, dass es keinen einzigen "Ansatz" gibt, um das Problem sauber zu lösen. Aber "so gut wie möglich zu testen" setzt die Messlatte sehr niedrig an. Ich werde mit meinen Ergebnissen zurückkommen.

0 Stimmen

In Java: Das Paket java.util.concurrent enthält einige schlecht bekannte Klassen, die helfen können, deterministische JUnit-Tests zu schreiben. Werfen Sie einen Blick auf - CountDownLatch - Semaphor - Austauscher

0 Stimmen

Können Sie bitte einen Link zu Ihrer vorherigen Frage zu Unit-Tests angeben?

19voto

Johan Punkte 32850

Warteschleife kann auch nützlich sein, um Ihnen zu helfen, deterministische Unit-Tests zu schreiben. Damit können Sie warten, bis irgendwo in Ihrem System ein Zustand aktualisiert wird. Zum Beispiel:

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

oder

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

Es bietet auch Unterstützung für Scala und Groovy.

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

19voto

xagyg Punkte 9266

Es gibt ein paar recht gute Tools. Hier ist eine Übersicht über einige der Java-Tools.

Einige gute Werkzeuge für die statische Analyse sind FindBugs (gibt einige nützliche Hinweise), JLint , Java-Pfadfinder (JPF & JPF2), und Bogor .

MultithreadingTC ist ein recht gutes dynamisches Analysewerkzeug (integriert in JUnit), mit dem Sie Ihre eigenen Testfälle erstellen können.

ConTest von IBM Research ist interessant. Es instrumentiert Ihren Code, indem es alle Arten von Thread-modifizierendem Verhalten (z.B. sleep & yield) einfügt, um zu versuchen, Fehler zufällig aufzudecken.

SPIN ist ein wirklich cooles Werkzeug für die Modellierung Ihrer Java- (und anderer) Komponenten, aber Sie brauchen ein nützliches Framework. Es ist schwer zu benutzen, aber extrem leistungsfähig, wenn Sie wissen, wie man es benutzt. Eine ganze Reihe von Tools verwenden SPIN unter der Haube.

MultithreadedTC ist wahrscheinlich das am weitesten verbreitete Programm, aber einige der oben aufgeführten statischen Analysetools sind auf jeden Fall einen Blick wert.

17voto

Robert Gould Punkte 66858

Eine andere Möglichkeit, Threaded Code und sehr komplexe Systeme im Allgemeinen (irgendwie) zu testen, ist durch Fuzz-Tests . Es ist nicht großartig, und es wird nicht alles finden, aber es wird wahrscheinlich nützlich sein, und es ist einfach zu tun.

Zitat:

Fuzz-Testing oder Fuzzing ist eine Software-Testtechnik, bei der die Eingaben eines Programms mit Zufallsdaten ("Fuzz") versehen werden. Wenn das Programm fehlschlägt (z. B. durch Absturz oder durch Nichteinhaltung eingebauter Code-Assertions), können die Fehler festgestellt werden. Der große Vorteil von Fuzz-Tests ist, dass der Testentwurf extrem einfach und frei von vorgefassten Meinungen über das Systemverhalten ist.

...

Fuzz-Tests werden häufig in großen Softwareentwicklungsprojekten eingesetzt, die Black-Box-Tests verwenden. Diese Projekte verfügen in der Regel über ein Budget für die Entwicklung von Testtools, und Fuzz-Testing ist eine der Techniken, die ein hohes Nutzen-Kosten-Verhältnis bietet.

...

Fuzz-Tests sind jedoch kein Ersatz für ausführliche Tests oder formale Methoden: Sie können nur eine Stichprobe des Systemverhaltens liefern, und in vielen Fällen kann das Bestehen eines Fuzz-Tests nur zeigen, dass ein Stück Software Ausnahmen ohne Absturz behandelt, anstatt sich korrekt zu verhalten. Fuzz-Tests können daher nur als Werkzeug zur Fehlersuche und nicht als Qualitätsgarantie betrachtet werden.

14voto

bennidi Punkte 1995

Die Prüfung von MT-Code auf Korrektheit ist, wie bereits erwähnt, ein ziemlich schwieriges Problem. Letztendlich geht es darum, sicherzustellen, dass es in Ihrem Code keine falsch synchronisierten Datenläufe gibt. Das Problem dabei ist, dass es unendlich viele Möglichkeiten der Thread-Ausführung (Verschachtelungen) gibt, über die Sie keine Kontrolle haben (lesen Sie unbedingt cette Artikel). In einfachen Szenarien mag es möglich sein, die Korrektheit tatsächlich durch Argumentation zu beweisen, aber das ist normalerweise nicht der Fall. Vor allem, wenn man die Synchronisierung vermeiden/minimieren und nicht die offensichtlichste/einfachste Synchronisierungsoption wählen möchte.

Ein Ansatz, den ich verfolge, besteht darin, hochgradig nebenläufigen Testcode zu schreiben, um das Auftreten potenziell unentdeckter Datenrennen wahrscheinlich zu machen. Und dann lasse ich diese Tests einige Zeit lang laufen :) Ich bin einmal über einen Vortrag gestolpert, in dem ein Informatiker ein Tool vorstellte, das genau das tut (zufällige Erstellung von Tests auf der Grundlage von Spezifikationen und anschließendes wildes, gleichzeitiges Ausführen, um zu prüfen, ob die definierten Invarianten gebrochen werden).

Übrigens glaube ich, dass dieser Aspekt des Testens von MT-Code hier noch nicht erwähnt wurde: die Identifizierung von Invarianten des Codes, auf die man zufällig prüfen kann. Leider ist das Finden dieser Invarianten auch ein ziemlich schwieriges Problem. Außerdem gelten sie nicht immer während der Ausführung, so dass man Ausführungspunkte finden/erzwingen muss, bei denen man erwarten kann, dass sie wahr sind. Die Ausführung des Codes in einen solchen Zustand zu bringen, ist ebenfalls ein schwieriges Problem (und kann selbst zu Problemen mit der Gleichzeitigkeit führen. Puh, das ist verdammt schwer!

Einige interessante Links zum Lesen:

13voto

Kevin Wong Punkte 14146

Ich habe das schon oft gemacht, und ja, es ist ätzend.

Einige Tipps:

  • GroboUtils für die Ausführung mehrerer Test-Threads
  • alphaWorks ConTest Klassen zu instrumentieren, damit die Verschachtelungen zwischen den Iterationen variieren
  • Erstellen einer throwable und überprüfen Sie es in tearDown (siehe Listing 1). Wenn Sie eine schlechte Ausnahme in einem anderen Thread abfangen, weisen Sie sie einfach throwable zu.
  • Ich habe die utils-Klasse in Listing 2 erstellt und habe festgestellt, dass sie von unschätzbarem Wert ist, insbesondere waitForVerify und waitForCondition, die die Leistung Ihrer Tests erheblich steigern werden.
  • Nutzen Sie die AtomicBoolean in Ihren Tests. Es ist thread-sicher, und Sie werden oft einen finalen Referenztyp benötigen, um Werte von Callback-Klassen und ähnlichem zu speichern. Siehe das Beispiel in Listing 3.
  • Stellen Sie sicher, dass Ihr Test immer eine Zeitüberschreitung enthält (z.B., @Test(timeout=60*1000) ), da Gleichzeitigkeitstests manchmal ewig hängen bleiben können, wenn sie kaputt sind.

Auflistung 1:

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

Auflistung 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;
    }
}
}

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

CodeJaeger.com

CodeJaeger ist eine Gemeinschaft für Programmierer, die täglich Hilfe erhalten..
Wir haben viele Inhalte, und Sie können auch Ihre eigenen Fragen stellen oder die Fragen anderer Leute lösen.

Powered by:

X