Come devo testare il codice thread unitario?


704

Finora ho evitato l'incubo che sta testando il codice multi-thread dal momento che sembra troppo un campo minato. Vorrei chiedere in che modo le persone hanno testato il codice che si basa sui thread per l'esecuzione corretta, o come le persone hanno provato quei tipi di problemi che si presentano solo quando due thread interagiscono in un determinato modo?

Questo sembra un problema davvero chiave per i programmatori oggi, sarebbe utile mettere insieme le nostre conoscenze su questo imho.


2
Stavo pensando di pubblicare una domanda su questo stesso identico problema. Mentre Will espone molti dei buoni punti di seguito, penso che possiamo fare di meglio. Sono d'accordo che non esiste un unico "approccio" per affrontarlo in modo pulito. Tuttavia, "test il meglio che puoi" sta impostando la barra molto bassa. Tornerò con le mie scoperte.
Zach Burlingame,

In Java: il pacchetto java.util.concurrent contiene alcune classi conosciute non valide, che possono aiutare a scrivere test JUnit deterministici.
Dai

Potete fornire un link alla domanda precedente relativa ai test unitari, per favore?
Andrew Grimm,


7
Penso che sia importante notare che questa domanda ha 8 anni e che nel frattempo le librerie di applicazioni hanno fatto molta strada. Nell'era moderna (2016) lo sviluppo multi-thread si manifesta principalmente nei sistemi embedded. Ma se stai lavorando su un'app desktop o telefonica, esplora prima le alternative. Gli ambienti applicativi come .NET ora includono strumenti per gestire o semplificare notevolmente probabilmente il 90% degli scenari multi-thread comuni. (asnync / waitit, PLinq, IObservable, il TPL ...). Il codice multi-thread è difficile. Se non reinventare la ruota, non è necessario ripetere il test.
Paul Williams,

Risposte:


245

Guarda, non c'è modo semplice per farlo. Sto lavorando a un progetto intrinsecamente multithread. Gli eventi arrivano dal sistema operativo e devo elaborarli contemporaneamente.

Il modo più semplice per gestire i test di codici applicativi complessi e multithread è questo: se è troppo complesso per essere testato, lo stai facendo male. Se si dispone di una singola istanza con più thread che agiscono su di essa e non è possibile verificare situazioni in cui questi thread si sovrappongono, è necessario ripetere il progetto. È tanto semplice quanto complesso.

Esistono molti modi per programmare il multithreading che evita che i thread eseguano le istanze contemporaneamente. Il più semplice è rendere immutabili tutti i tuoi oggetti. Certo, di solito non è possibile. Quindi devi identificare quei luoghi nel tuo design in cui i thread interagiscono con la stessa istanza e ridurre il numero di quei luoghi. In questo modo, si isolano alcune classi in cui si verifica effettivamente il multithreading, riducendo la complessità complessiva del test del sistema.

Ma devi capire che anche così facendo non riesci ancora a testare tutte le situazioni in cui due thread si avvicinano. Per fare ciò, dovresti eseguire due thread contemporaneamente nello stesso test, quindi controllare esattamente quali linee stanno eseguendo in un dato momento. Il meglio che puoi fare è simulare questa situazione. Ma questo potrebbe richiedere di programmare specificamente per i test, e questo è nella migliore delle ipotesi un mezzo passo verso una vera soluzione.

Probabilmente il modo migliore per testare il codice per problemi di threading è attraverso l'analisi statica del codice. Se il tuo codice thread non segue un set finito di pattern thread-safe, potresti avere un problema. Credo che l'analisi del codice in VS contenga una certa conoscenza del threading, ma probabilmente non molto.

Guarda, allo stato attuale delle cose (e probabilmente rimarrà un buon momento a venire), il modo migliore per testare le app multithreading è ridurre il più possibile la complessità del codice thread. Ridurre al minimo le aree in cui i thread interagiscono, testare il meglio possibile e utilizzare l'analisi del codice per identificare le aree pericolose.


1
L'analisi del codice è ottima se hai a che fare con un linguaggio / framework che lo consente. Ad esempio: Findbugs troverà problemi di concorrenza condivisi molto semplici e facili con variabili statiche. Ciò che non riesce a trovare sono i modelli di progettazione singleton, presuppone che tutti gli oggetti possano essere creati più volte. Questo plugin è tristemente inadeguato per framework come Spring.
Zombi,

3
esiste in realtà una cura: oggetti attivi. drdobbs.com/parallel/prefer-using-active-objects-instead-of-n/…
Dill

6
Mentre questo è un buon consiglio, mi rimane ancora da chiedere: "Come posso testare quelle aree minime in cui sono richiesti più thread?"
Bryan Rayner,

5
"Se è troppo complesso per essere testato, stai sbagliando" - tutti dobbiamo immergerci nel codice legacy che non abbiamo scritto. In che modo questa osservazione aiuta esattamente chiunque?
Ronna,

2
L'analisi statica è probabilmente una buona idea, ma non sta testando. Questo post in realtà non risponde alla domanda, che riguarda come testare.
Warren Dew,

96

È passato un po 'di tempo quando questa domanda è stata pubblicata, ma non ha ancora ricevuto risposta ...

La risposta di kleolb02 è buona. Proverò ad entrare in maggiori dettagli.

C'è un modo, che pratico per il codice C #. Per i test unitari dovresti essere in grado di programmare test riproducibili , che è la più grande sfida nel codice multithread. Quindi la mia risposta mira a forzare il codice asincrono in un cablaggio di prova, che funziona in modo sincrono .

È un'idea tratta dal libro di Gerard Meszardos " xUnit Test Patterns " e si chiama "Humble Object" (p. 695): devi separare il codice della logica di base e tutto ciò che profuma di codice asincrono l'uno dall'altro. Ciò porterebbe a una classe per la logica principale, che funziona in modo sincrono .

Questo ti mette nella posizione di testare il codice logico di base in modo sincrono . Hai il controllo assoluto sulla tempistica delle chiamate che stai facendo sulla logica di base e quindi puoi effettuare test riproducibili . E questo è il tuo vantaggio dalla separazione della logica di base e della logica asincrona.

Questa logica di base deve essere racchiusa da un'altra classe, che è responsabile della ricezione asincrona delle chiamate alla logica principale e delega queste chiamate alla logica principale. Il codice di produzione accederà alla logica principale solo tramite quella classe. Poiché questa classe dovrebbe delegare solo le chiamate, è una classe molto "stupida" senza molta logica. In questo modo è possibile mantenere al minimo i test unitari per questa classe operaia asincrona.

Tutto quanto sopra (test dell'interazione tra le classi) sono test componenti. Anche in questo caso, dovresti essere in grado di avere il controllo assoluto sui tempi, se ti attieni al modello "Humble Object".


1
Ma a volte se i fili collaborano bene tra loro è anche qualcosa che dovrebbe essere testato, giusto? Sicuramente separerò la logica di base dalla parte asincrona dopo aver letto la tua risposta. Ma continuerò a testare la logica tramite interfacce asincrone con un callback di lavoro su tutti i thread.
CopperCash,

Che dire dei sistemi multiprocessore?
Tecnophile,

65

Difficile davvero! Nei miei test unit (C ++), l'ho suddiviso in diverse categorie lungo le linee del modello di concorrenza utilizzato:

  1. Test unitari per le classi che operano in un singolo thread e non sono consapevoli del thread: facile, testare come al solito.

  2. Test unitari per oggetti Monitor (quelli che eseguono metodi sincronizzati nel thread di controllo dei chiamanti) che espongono un'API pubblica sincronizzata - istanziano più thread simulati che esercitano l'API. Costruisci scenari che esercitano le condizioni interne dell'oggetto passivo. Includi un test in esecuzione più lungo che in sostanza lo supera da più thread per un lungo periodo di tempo. Questo non è scientifico, lo so, ma crea fiducia.

  3. Test unitari per oggetti attivi (quelli che incapsulano il proprio thread o thread di controllo) - simile al n. 2 sopra con variazioni a seconda del design della classe. L'API pubblica può essere bloccante o non bloccante, i chiamanti possono ottenere futures, i dati possono arrivare in coda o devono essere rimossi. Ci sono molte combinazioni possibili qui; scatola bianca di distanza. Richiede ancora più thread simulati per effettuare chiamate all'oggetto in prova.

A parte:

Nella formazione interna per sviluppatori che faccio, insegno i pilastri della concorrenza e questi due modelli come quadro principale per pensare e scomporre i problemi di concorrenza. Ovviamente ci sono concetti più avanzati là fuori, ma ho scoperto che questo insieme di basi aiuta a tenere gli ingegneri fuori dalla zuppa. Porta anche a un codice che è più testabile dall'unità, come descritto sopra.


51

Ho affrontato questo problema diverse volte negli ultimi anni quando ho scritto il codice di gestione dei thread per diversi progetti. Sto fornendo una risposta tardiva perché la maggior parte delle altre risposte, pur fornendo alternative, in realtà non rispondono alla domanda sui test. La mia risposta è indirizzata ai casi in cui non esiste alternativa al codice multithread; Copro i problemi di progettazione del codice per completezza, ma discuto anche dei test unitari.

Scrittura di codice multithread verificabile

La prima cosa da fare è separare il codice di gestione del thread di produzione da tutto il codice che esegue l'elaborazione effettiva dei dati. In questo modo, l'elaborazione dei dati può essere testata come codice a thread singolo e l'unica cosa che fa il codice multithread è coordinare i thread.

La seconda cosa da ricordare è che i bug nel codice multithread sono probabilistici; i bug che si manifestano meno frequentemente sono i bug che entreranno di soppiatto nella produzione, che saranno difficili da riprodurre anche in fase di produzione e causeranno quindi i maggiori problemi. Per questo motivo, l'approccio di codifica standard di scrivere rapidamente il codice e quindi eseguirne il debug fino a quando funziona non è una cattiva idea per il codice multithread; si tradurrà in codice in cui vengono corretti i bug facili e i bug pericolosi sono ancora lì.

Invece, quando si scrive codice multithread, è necessario scrivere il codice con l'atteggiamento che si eviterà di scrivere i bug in primo luogo. Se hai rimosso correttamente il codice di elaborazione dei dati, il codice di gestione dei thread dovrebbe essere abbastanza piccolo - preferibilmente poche righe, nel peggiore dei casi una dozzina di righe - che hai la possibilità di scriverlo senza scrivere un bug e certamente senza scrivere molti bug , se capisci il threading, prenditi il ​​tuo tempo e stai attento.

Scrittura di unit test per codice multithread

Una volta che il codice multithread è stato scritto nel modo più accurato possibile, vale comunque la pena scrivere test per quel codice. Lo scopo principale dei test non è tanto quello di testare i bug delle condizioni di gara altamente dipendenti dal tempo - è impossibile testare ripetutamente tali condizioni di gara - ma piuttosto testare che la vostra strategia di blocco per prevenire tali bug consenta a più thread di interagire come previsto .

Per testare correttamente il corretto comportamento di blocco, un test deve avviare più thread. Per rendere ripetibile il test, vogliamo che le interazioni tra i thread avvengano in un ordine prevedibile. Non vogliamo sincronizzare esternamente i thread nel test, perché ciò maschererà i bug che potrebbero verificarsi nella produzione in cui i thread non sono sincronizzati esternamente. Ciò lascia l'uso dei ritardi temporali per la sincronizzazione dei thread, che è la tecnica che ho usato con successo ogni volta che ho dovuto scrivere test di codice multithread.

Se i ritardi sono troppo brevi, il test diventa fragile, perché differenze di temporizzazione minori, ad esempio tra macchine diverse su cui è possibile eseguire i test, possono causare l'interruzione del tempo e il fallimento del test. Quello che ho fatto in genere è iniziare con ritardi che causano errori nei test, aumentano i ritardi in modo che il test passi in modo affidabile sulla mia macchina di sviluppo e quindi raddoppiano i ritardi oltre a ciò in modo che il test abbia buone probabilità di passare ad altre macchine. Ciò significa che il test richiederà una quantità macroscopica di tempo, anche se nella mia esperienza, un'attenta progettazione del test può limitare quel tempo a non più di una dozzina di secondi. Dal momento che non dovresti avere molti posti che richiedono il codice di coordinamento del thread nella tua applicazione, questo dovrebbe essere accettabile per la tua suite di test.

Infine, tieni traccia del numero di bug rilevati dal tuo test. Se il test ha una copertura del codice dell'80%, è possibile che si verifichino circa l'80% dei bug. Se il test è ben progettato ma non rileva alcun bug, è probabile che non si disponga di altri bug che verranno visualizzati solo in produzione. Se il test rileva uno o due bug, potresti comunque essere fortunato. Oltre a ciò, e potresti voler prendere in considerazione un'attenta revisione o addirittura una completa riscrittura del codice di gestione dei thread, poiché è probabile che il codice contenga ancora bug nascosti che saranno molto difficili da trovare fino a quando il codice non sarà in produzione, e molto difficile da risolvere allora.


3
I test possono solo rivelare la presenza di bug, non la loro assenza. La domanda originale pone un problema a 2 thread, nel qual caso potrebbero essere possibili test approfonditi, ma spesso non lo è. Per qualsiasi cosa al di là degli scenari più semplici potresti dover mordere il proiettile e usare metodi formali, ma non saltare i test unitari! Scrivere correttamente il codice multi-thread è difficile in primo luogo, ma un problema altrettanto difficile è renderlo a prova di futuro contro la regressione.
Paul Williams,

4
Incredibile riassunto di uno dei modi meno compresi. La tua risposta è la vera segregazione che generalmente i ppl trascurano.
Prash

1
Una dozzina di secondi è un bel po 'di tempo, anche se hai solo poche centinaia di test di quella lunghezza ...
Toby Speight,

1
@TobySpeight I test sono lunghi rispetto ai normali test unitari. Ho scoperto che una mezza dozzina di test sono più che sufficienti se il codice threaded è correttamente progettato per essere il più semplice possibile, sebbene - la necessità di alcune centinaia di test multithreading indicherebbe quasi certamente una disposizione di threading troppo complessa.
Warren Dew,

2
Questo è un buon argomento per mantenere la logica del thread il più separabile possibile dalla funzionalità (lo so, molto più facile a dirsi che a farsi). E, se possibile, suddividere la suite di test in insiemi "ogni cambiamento" e "pre-commit" (quindi i test minuto per minuto non vengono influenzati troppo).
Toby Speight,

22

Ho anche avuto seri problemi nel test del codice multi-thread. Poi ho trovato una soluzione davvero interessante in "xUnit Test Patterns" di Gerard Meszaros. Il modello che descrive è chiamato oggetto umile .

Fondamentalmente descrive come estrarre la logica in un componente separato, facile da testare e disaccoppiato dal suo ambiente. Dopo aver testato questa logica, è possibile verificare il comportamento complicato (multi-threading, esecuzione asincrona, ecc ...)


20

Ci sono alcuni strumenti in giro che sono abbastanza buoni. Ecco un riepilogo di alcuni di quelli Java.

Alcuni buoni strumenti di analisi statica includono FindBugs (fornisce alcuni suggerimenti utili), JLint , Java Pathfinder (JPF e JPF2) e Bogor .

MultithreadedTC è un ottimo strumento di analisi dinamica (integrato in JUnit) in cui devi impostare i tuoi casi di test.

ConTest di IBM Research è interessante. Strumenta il tuo codice inserendo tutti i tipi di comportamenti di modifica dei thread (ad es. Sleep & yield) per cercare di scoprire i bug in modo casuale.

SPIN è uno strumento davvero interessante per modellare i componenti Java (e altri), ma è necessario disporre di un framework utile. È difficile da usare così com'è, ma estremamente potente se sai come usarlo. Molti strumenti usano SPIN sotto il cofano.

MultithreadedTC è probabilmente il più mainstream, ma alcuni degli strumenti di analisi statica sopra elencati meritano sicuramente una visita.


16

Anche l'attendibilità può essere utile per aiutarti a scrivere test unitari deterministici. Ti consente di attendere fino a quando uno stato da qualche parte nel tuo sistema viene aggiornato. Per esempio:

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

o

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

Ha anche il supporto di Scala e Groovy.

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

1
L'attesa è geniale, esattamente quello che stavo cercando!
Forge_7,

14

Un altro modo per (kinda) testare il codice threaded e sistemi molto complessi in generale è attraverso il Fuzz Testing . Non è eccezionale e non troverà tutto, ma è probabilmente utile e semplice da fare.

Citazione:

Il fuzz testing o fuzzing è una tecnica di test software che fornisce dati casuali ("fuzz") agli input di un programma. Se il programma ha esito negativo (ad esempio, in seguito a un arresto anomalo o in mancanza di asserzioni di codice integrate), è possibile notare i difetti. Il grande vantaggio dei test fuzz è che il design del test è estremamente semplice e privo di preconcetti sul comportamento del sistema.

...

Il test Fuzz viene spesso utilizzato in grandi progetti di sviluppo software che utilizzano test in scatola nera. Questi progetti di solito hanno un budget per sviluppare strumenti di test e il test fuzz è una delle tecniche che offre un elevato rapporto costi / benefici.

...

Tuttavia, il test fuzz non sostituisce test esaustivi o metodi formali: può solo fornire un campione casuale del comportamento del sistema e in molti casi il superamento di un test fuzz può solo dimostrare che un software gestisce le eccezioni senza crash, piuttosto che comportarsi correttamente. Pertanto, il test fuzz può essere considerato solo uno strumento per la ricerca di bug piuttosto che una garanzia di qualità.


13

Ho fatto molto di tutto questo, e sì fa schifo.

Alcuni suggerimenti:

  • GroboUtils per l'esecuzione di più thread di test
  • alphaWorks ConTest per le classi di strumenti per far sì che gli intrecci possano variare tra iterazioni
  • Crea un throwablecampo e registralo tearDown(vedi il Listato 1). Se riscontri una cattiva eccezione in un altro thread, assegnalo a lanciabile.
  • Ho creato la classe utils nel Listato 2 e l'ho trovata preziosa, in particolare waitForVerify e waitForCondition, che aumenterà notevolmente le prestazioni dei tuoi test.
  • Fai buon uso AtomicBooleannei tuoi test. È thread-safe e spesso avrai bisogno di un tipo di riferimento finale per memorizzare i valori delle classi di callback e simili. Vedi esempio nel Listato 3.
  • Assicurati di dare sempre un timeout al tuo test (ad esempio, @Test(timeout=60*1000)), poiché a volte i test di concorrenza possono bloccarsi per sempre quando vengono interrotti.

Elenco 1:

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

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

Elenco 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 timeout è una buona idea, ma se un test scade, qualsiasi risultato successivo in tale esecuzione è sospetto. Il test scaduto potrebbe avere ancora alcuni thread in esecuzione che potrebbero rovinarti.
Don Kirkby,

12

Verificare la correttezza del codice MT è, come già detto, un problema piuttosto difficile. Alla fine si riduce a garantire che non ci siano corse di dati sincronizzate in modo errato nel codice. Il problema è che ci sono infinite possibilità di esecuzione di thread (intrecci) su cui non hai molto controllo (assicurati di leggere questo articolo, però). In scenari semplici potrebbe essere possibile dimostrare effettivamente la correttezza ragionando, ma di solito non è così. Soprattutto se si desidera evitare / ridurre al minimo la sincronizzazione e non scegliere l'opzione di sincronizzazione più ovvia / più semplice.

Un approccio che seguo è quello di scrivere un codice di test altamente concorrenziale al fine di rendere probabili gare di dati potenzialmente non rilevati. E poi ho eseguito quei test per un po 'di tempo :) Una volta mi sono imbattuto in un discorso in cui un informatico in cui mostrava uno strumento che fa questo (escogitando casualmente test dalle specifiche e quindi eseguendoli selvaggiamente, contemporaneamente, controllando gli invarianti definiti essere rotto).

A proposito, penso che questo aspetto del test del codice MT non sia stato menzionato qui: identifica gli invarianti del codice che puoi controllare casualmente. Sfortunatamente, trovare quegli invarianti è anche un problema piuttosto difficile. Inoltre, potrebbero non trattenersi tutto il tempo durante l'esecuzione, quindi devi trovare / applicare punti di esecuzione in cui puoi aspettarti che siano veri. Portare l'esecuzione del codice in tale stato è anche un problema difficile (e potrebbe anche comportare problemi di concorrenza. Accidenti, è dannatamente difficile!

Alcuni link interessanti da leggere:


l'autore si riferisce alla randomizaion nei test. Potrebbe essere QuickCheck , che è stato portato in molte lingue. Puoi guardare i discorsi su tali test per il sistema simultaneo qui
Max

6

Pete Goodliffe ha una serie sull'unità di test del codice thread .

È difficile. Prendo la via più semplice e provo a mantenere il codice di threading estratto dal test effettivo. Pete dice che il modo in cui lo faccio è sbagliato, ma ho avuto la separazione giusta o sono stato solo fortunato.


6
Ho letto i due articoli pubblicati finora e non li ho trovati molto utili. Parla solo delle difficoltà senza dare molti consigli concreti. Forse i futuri articoli miglioreranno.
Don Kirkby,

6

Per Java, consultare il capitolo 12 di JCIP . Ci sono alcuni esempi concreti di scrittura di test unitari deterministici e multi-thread per testare almeno la correttezza e gli invarianti del codice concorrente.

"Provare" la sicurezza del thread con i test unitari è molto più naturale. La mia convinzione è che questo sia meglio servito dai test di integrazione automatizzata su una varietà di piattaforme / configurazioni.


6

Mi piace scrivere due o più metodi di test da eseguire su thread paralleli e ciascuno di essi effettua chiamate nell'oggetto in prova. Ho usato le chiamate Sleep () per coordinare l'ordine delle chiamate dai diversi thread, ma non è molto affidabile. È anche molto più lento perché devi dormire abbastanza a lungo che i tempi di solito funzionano.

Ho trovato la libreria Multithreaded TC Java dello stesso gruppo che ha scritto FindBugs. Ti consente di specificare l'ordine degli eventi senza usare Sleep () ed è affidabile. Non l'ho ancora provato.

Il limite più grande a questo approccio è che ti consente solo di testare gli scenari che sospetti possano causare problemi. Come altri hanno già detto, devi davvero isolare il tuo codice multithread in un piccolo numero di semplici classi per avere la speranza di testarli a fondo.

Dopo aver testato attentamente gli scenari che ti aspetti di causare problemi, un test non scientifico che lancia un gruppo di richieste simultanee alla classe per un po 'è un buon modo per cercare problemi inaspettati.

Aggiornamento: ho giocato un po 'con la libreria Multithreaded TC Java e funziona bene. Ho anche portato alcune delle sue funzionalità su una versione .NET che chiamo TickingTest .


5

Gestisco i test unitari dei componenti filettati allo stesso modo in cui gestisco qualsiasi test unitario, ovvero con l'inversione dei quadri di controllo e di isolamento. Sviluppo nell'arena .Net e fuori dagli schemi il threading (tra le altre cose) è molto difficile (direi quasi impossibile) da isolare completamente.

Pertanto ho scritto wrapper che assomigliano a questo (semplificato):

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

Da lì posso facilmente iniettare IThreadingManager nei miei componenti e utilizzare la mia struttura di isolamento preferita per far sì che il thread si comporti come mi aspetto durante il test.

Finora ha funzionato benissimo per me e uso lo stesso approccio per il pool di thread, cose in System.Environment, Sleep ecc. Ecc.


5

Dai un'occhiata alla mia risposta correlata a

Progettazione di una classe di test per una barriera personalizzata

È distorto verso Java ma ha un ragionevole sommario delle opzioni.

In sintesi (IMO) non è l'uso di un framework sofisticato che garantirà la correttezza ma il modo in cui si procede alla progettazione del codice multithread. Dividere le preoccupazioni (concorrenza e funzionalità) è un modo enorme per aumentare la fiducia. Il software orientato agli oggetti in crescita guidato da test spiega alcune opzioni meglio di me.

L'analisi statica e i metodi formali (vedi Concorrenza: modelli statali e programmi Java ) sono un'opzione, ma li ho trovati di utilità limitata nello sviluppo commerciale.

Non dimenticare che i test di stile carico / ammollo raramente sono garantiti per evidenziare i problemi.

In bocca al lupo!


Dovresti anche menzionare la tua tempus-fugitbiblioteca qui, che helps write and test concurrent code;)
Idolon,

4

Di recente ho scoperto (per Java) uno strumento chiamato Threadsafe. È uno strumento di analisi statica molto simile a findbugs ma in particolare per individuare problemi multi-threading. Non è un sostituto per i test, ma posso consigliarlo come parte della scrittura di Java multi-thread affidabili.

Cattura anche alcuni potenziali problemi molto sottili intorno a cose come l'assunzione di classi, l'accesso a oggetti non sicuri attraverso classi concorrenti e l'individuazione di modificatori volatili mancanti quando si utilizza il paradigma di blocco a doppio controllo.

Se scrivi Java multithreading provalo .


3

Il seguente articolo suggerisce 2 soluzioni. Avvolge un semaforo (CountDownLatch) e aggiunge funzionalità come esternalizzare i dati dal thread interno. Un altro modo per raggiungere questo scopo è utilizzare il pool di thread (vedere Punti di interesse).

Sprinkler: oggetto di sincronizzazione avanzata


3
Spiega qui gli approcci, i collegamenti esterni potrebbero essere morti in futuro.
Uooo,

2

Ho trascorso gran parte della scorsa settimana in una biblioteca universitaria a studiare il debug di codice simultaneo. Il problema centrale è che il codice concorrente non è deterministico. In genere, il debug accademico è caduto in uno dei tre campi qui:

  1. Evento-traccia / replay. Ciò richiede un monitoraggio degli eventi e quindi la revisione degli eventi inviati. In un framework UT, ciò implicherebbe l'invio manuale degli eventi come parte di un test e quindi l'esecuzione di revisioni post mortem.
  2. Script. Qui è dove interagisci con il codice in esecuzione con una serie di trigger. "Su x> pippo, baz ()". Questo potrebbe essere interpretato in un framework UT in cui è presente un sistema di runtime che attiva un determinato test a una determinata condizione.
  3. Interactive. Questo ovviamente non funzionerà in una situazione di test automatico. ;)

Ora, come notato sopra dai commentatori, puoi progettare il tuo sistema simultaneo in uno stato più deterministico. Tuttavia, se non lo fai correttamente, sei appena tornato a progettare di nuovo un sistema sequenziale.

Il mio suggerimento sarebbe quello di concentrarmi sull'avere un protocollo di progettazione molto rigoroso su ciò che viene thread e ciò che non viene thread. Se si vincola l'interfaccia in modo che vi siano dipendenze minime tra gli elementi, è molto più semplice.

Buona fortuna e continua a lavorare sul problema.


2

Ho avuto lo sfortunato compito di testare il codice thread e sono sicuramente i test più difficili che abbia mai scritto.

Quando ho scritto i miei test, ho usato una combinazione di delegati ed eventi. Fondamentalmente si tratta di utilizzare PropertyNotifyChangedeventi con uno WaitCallbacko qualche tipo di ConditionalWaitertali sondaggi.

Non sono sicuro che questo fosse l'approccio migliore, ma ha funzionato per me.


1

Supponendo che il codice "multi-thread" intendesse qualcosa

  • stato e mutevole
  • E accessibile / modificato da più thread contemporaneamente

In altre parole, stiamo parlando di testare una classe / metodo / unità con thread stateful personalizzati, che al giorno d'oggi dovrebbe essere una bestia molto rara.

Poiché questa bestia è rara, prima di tutto dobbiamo assicurarci che ci siano tutte le scuse valide per scriverlo.

Passaggio 1. Considerare di modificare lo stato nello stesso contesto di sincronizzazione.

Oggi è facile scrivere codice simultaneo e asincrono compositivo in cui IO o altre operazioni lente sono state scaricate in background ma lo stato condiviso viene aggiornato e interrogato in un contesto di sincronizzazione. ad esempio compiti asincroni / attendi e Rx in .NET ecc. - sono tutti testabili in base alla progettazione, attività e pianificatori "reali" possono essere sostituiti per rendere deterministici i test (tuttavia ciò non rientra nell'ambito della domanda).

Può sembrare molto limitato ma questo approccio funziona sorprendentemente bene. È possibile scrivere intere app in questo stile senza bisogno di rendere sicuro il thread (lo faccio).

Passaggio 2. Se la manipolazione dello stato condiviso su un singolo contesto di sincronizzazione non è assolutamente possibile.

Assicurati che la ruota non venga reinventata / non esiste sicuramente alcuna alternativa standard che possa essere adattata per il lavoro. Dovrebbe essere probabile che il codice sia molto coerente e contenuto all'interno di un'unità, ad es. Con buone probabilità che sia un caso speciale di una struttura di dati standard thread-safe come hash map o collection o quant'altro.

Nota: se il codice è grande / si estende su più classi E necessita di manipolazione dello stato multi-thread, allora c'è una probabilità molto alta che il design non sia buono, riconsiderare il Passaggio 1

Passaggio 3. Se viene raggiunto questo passaggio, è necessario testare la propria classe / metodo / unità con thread-stateful personalizzati .

Sarò assolutamente onesto: non ho mai dovuto scrivere test adeguati per tale codice. La maggior parte delle volte vado via al passaggio 1, a volte al passaggio 2. L'ultima volta che ho dovuto scrivere un codice thread-safe personalizzato è stato così tanti anni fa che è stato prima di adottare il test unitario / probabilmente non avrei dovuto scriverlo con le attuali conoscenze comunque.

Se dovessi davvero testare tale codice ( finalmente, la risposta effettiva ), proverei un paio di cose di seguito

  1. Prove di stress non deterministiche. ad esempio, eseguire 100 thread contemporaneamente e verificare che il risultato finale sia coerente. Questo è più tipico per i test di livello superiore / integrazione di scenari con più utenti, ma può anche essere utilizzato a livello di unità.

  2. Esporre alcuni "hook" di test in cui test può iniettare del codice per aiutare a creare scenari deterministici in cui un thread deve eseguire un'operazione prima dell'altro. Per quanto brutto, non riesco a pensare a niente di meglio.

  3. Test guidato in base al ritardo per eseguire i thread ed eseguire operazioni in un ordine particolare. A rigor di termini, anche questi test non sono deterministici (c'è una possibilità di congelamento / arresto della raccolta GC del sistema che può distorcere ritardi altrimenti orchestrati), inoltre è brutto ma consente di evitare ganci.


0

Per il codice J2E, ho usato SilkPerformer, LoadRunner e JMeter per i test di concorrenza dei thread. Fanno tutti la stessa cosa. Fondamentalmente, ti offrono un'interfaccia relativamente semplice per amministrare la loro versione del server proxy, necessaria, al fine di analizzare il flusso di dati TCP / IP e simulare più utenti che fanno richieste simultanee al tuo server delle app. Il server proxy può darti la possibilità di fare cose come analizzare le richieste fatte, presentando l'intera pagina e l'URL inviati al server, nonché la risposta dal server, dopo aver elaborato la richiesta.

È possibile trovare alcuni bug in modalità http non sicura, in cui è possibile almeno analizzare i dati del modulo che viene inviato e modificarli sistematicamente per ciascun utente. Ma i veri test sono quando si esegue in https (Secured Socket Layers). Quindi, devi anche fare i conti con l'alterazione sistematica dei dati di sessione e cookie, che può essere un po 'più complicato.

Il miglior bug che abbia mai trovato, durante il test della concorrenza, è stato quando ho scoperto che lo sviluppatore si era affidato alla raccolta di dati inutili Java per chiudere la richiesta di connessione stabilita al momento dell'accesso, al server LDAP, durante l'accesso. sessioni di altri utenti e risultati molto confusi, quando si cerca di analizzare ciò che è accaduto quando il server è stato messo in ginocchio, a malapena in grado di completare una transazione, ogni pochi secondi.

Alla fine, tu o qualcuno dovrete probabilmente allacciare e analizzare il codice per errori come quello che ho appena citato. E una discussione aperta tra i dipartimenti, come quella che si è verificata, quando abbiamo spiegato il problema sopra descritto, è molto utile. Ma questi strumenti sono la soluzione migliore per testare il codice multi-thread. JMeter è open source. SilkPerformer e LoadRunner sono proprietari. Se vuoi davvero sapere se la tua app è thread-safe, è così che lo fanno i ragazzi grandi. L'ho fatto professionalmente per aziende molto grandi, quindi non credo. Sto parlando per esperienza personale.

Un avvertimento: ci vuole del tempo per capire questi strumenti. Non si tratterà semplicemente di installare il software e di avviare la GUI, a meno che tu non abbia già avuto una certa esposizione alla programmazione multi-thread. Ho cercato di identificare le 3 categorie critiche di aree da comprendere (moduli, dati di sessione e cookie), con la speranza che almeno a partire dalla comprensione di questi argomenti ti aiuti a concentrarti su risultati rapidi, invece di dover leggere i intera documentazione.


0

La concorrenza è una complessa interazione tra il modello di memoria, l'hardware, le cache e il nostro codice. Nel caso di Java almeno tali test sono stati in parte affrontati principalmente da jcstress . I creatori di quella libreria sono noti per essere autori di molte funzionalità di concorrenza JVM, GC e Java.

Ma anche questa libreria richiede una buona conoscenza delle specifiche del modello di memoria Java in modo da sapere esattamente cosa stiamo testando. Ma penso che il focus di questo sforzo sia mircobenchmarks. Non grandi applicazioni aziendali.


0

C'è un articolo sull'argomento, usando Rust come lingua nel codice di esempio:

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

In sintesi, il trucco è quello di scrivere la tua logica concorrente in modo che sia solida per il non determinismo coinvolto in più thread di esecuzione, usando strumenti come canali e condvar.

Quindi, se è così che hai strutturato i tuoi "componenti", il modo più semplice per testarli è usare i canali per inviare loro messaggi e poi bloccare su altri canali per affermare che il componente invia determinati messaggi previsti.

L'articolo collegato è interamente scritto usando unit test.


-1

Se stai testando semplice nuovo thread (eseguibile) .run () Puoi deridere Thread per eseguire il eseguibile in sequenza

Ad esempio, se il codice dell'oggetto testato invoca un nuovo thread come questo

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

Quindi deridere nuovi thread ed eseguire l'argomento eseguibile in sequenza può aiutare

@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

(se possibile) non usare discussioni, usare attori / oggetti attivi. Facile da testare.


2
@OMTheEternity forse ma è comunque la migliore risposta imo.
Dill

-5

È possibile utilizzare EasyMock.makeThreadSafe per rendere sicuro il thread dell'istanza del test


Questo non è affatto un modo possibile di testare il codice multithread. Il problema non è che il codice di test esegue il multi-thread ma che si verifica il codice che di solito esegue il multi-thread. E non puoi sincronizzare tutto via perché in realtà non esegui più il test per le gare di dati.
bennidi,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.