Jak powinienem przetestować kod wątkowy?


704

Do tej pory unikałem koszmaru, który testuje wielowątkowy kod, ponieważ wydaje się, że to zbyt duże pole minowe. Chciałbym zapytać, w jaki sposób ludzie testowali kod, który opiera się na wątkach w celu pomyślnego wykonania, lub po prostu, jak ludzie testowali tego rodzaju problemy, które pojawiają się tylko wtedy, gdy dwa wątki oddziałują w określony sposób?

Wydaje się, że jest to dziś naprawdę kluczowy problem dla programistów, przydatne byłoby połączenie naszej wiedzy na temat tego jednego imho.


2
Myślałem o opublikowaniu pytania na ten sam temat. Podczas gdy Will przedstawia wiele dobrych punktów poniżej, myślę, że możemy to zrobić lepiej. Zgadzam się, że nie ma jednego „podejścia” do rozwiązania tego problemu w sposób czysty. Jednak „testowanie najlepiej jak potrafisz” stawia poprzeczkę bardzo nisko. Wrócę z moimi ustaleniami.
Zach Burlingame

W Javie: Pakiet java.util.concurrent zawiera kilka źle znanych klas, które mogą pomóc w pisaniu deterministycznych testów JUnit. Spójrz na - CountDownLatch - Semafor - Wymiennik
Synox

Czy możesz podać link do swojego poprzedniego pytania dotyczącego testów jednostkowych?
Andrew Grimm,


7
Myślę, że ważne jest, aby pamiętać, że to pytanie ma 8 lat, a tymczasem biblioteki aplikacji przeszły dość długą drogę. W „erze współczesnej” (2016) pojawia się wielowątkowy rozwój głównie w systemach wbudowanych. Ale jeśli pracujesz nad aplikacją na komputer lub telefon, najpierw sprawdź alternatywy. Środowiska aplikacji, takie jak .NET, zawierają teraz narzędzia do zarządzania lub znacznego uproszczenia prawdopodobnie 90% typowych scenariuszy wielowątkowych. (asnync / await, PLinq, IObservable, TPL ...). Kod wielowątkowy jest trudny. Jeśli nie wymyślisz koła na nowo, nie musisz go ponownie testować.
Paul Williams

Odpowiedzi:


245

Słuchaj, nie ma łatwego sposobu na zrobienie tego. Pracuję nad projektem z natury wielowątkowym. Zdarzenia przychodzą z systemu operacyjnego i muszę je przetwarzać jednocześnie.

Najprostszym sposobem radzenia sobie z testowaniem złożonego, wielowątkowego kodu aplikacji jest: Jeśli jest zbyt skomplikowany do przetestowania, robisz to źle. Jeśli masz jedną instancję, na którą działa wiele wątków, i nie możesz przetestować sytuacji, w których te wątki przebiegają jeden po drugim, projekt musi zostać przerobiony. Jest to tak proste, jak i tak złożone.

Istnieje wiele sposobów programowania wielowątkowości, które pozwalają uniknąć jednoczesnego uruchamiania wątków przez instancje. Najprostszym jest uczynienie wszystkich twoich obiektów niezmiennymi. Oczywiście nie jest to zazwyczaj możliwe. Musisz więc zidentyfikować miejsca w projekcie, w których wątki wchodzą w interakcje z tym samym wystąpieniem i zmniejszyć liczbę tych miejsc. W ten sposób izolujesz kilka klas, w których faktycznie występuje wielowątkowość, zmniejszając ogólną złożoność testowania systemu.

Ale musisz zdać sobie sprawę, że nawet robiąc to, nadal nie możesz przetestować każdej sytuacji, w której dwa wątki na siebie natrafiają. Aby to zrobić, musisz uruchomić dwa wątki jednocześnie w tym samym teście, a następnie dokładnie kontrolować, które wiersze wykonują w danym momencie. Najlepsze, co możesz zrobić, to zasymulować tę sytuację. Ale może to wymagać kodowania specjalnie do testowania, a to w najlepszym razie pół kroku w kierunku prawdziwego rozwiązania.

Prawdopodobnie najlepszym sposobem przetestowania kodu pod kątem problemów z wątkami jest statyczna analiza kodu. Jeśli Twój wątkowy kod nie jest zgodny ze skończonym zestawem bezpiecznych wątków, możesz mieć problem. Wierzę, że analiza kodu w VS zawiera pewną wiedzę na temat wątków, ale prawdopodobnie niewiele.

Spójrz, ponieważ sytuacja jest obecnie (i prawdopodobnie będzie to dobry moment, aby przyjść), najlepszym sposobem na przetestowanie aplikacji wielowątkowych jest zmniejszenie złożoności kodu wątkowego w jak największym stopniu. Minimalizuj obszary interakcji wątków, testuj jak najlepiej i wykorzystuj analizę kodu, aby identyfikować niebezpieczne obszary.


1
Analiza kodu jest świetna, jeśli masz do czynienia z językiem / strukturą, która na to pozwala. EG: Findbugs znajdzie bardzo proste i łatwe problemy z współdzieleniem ze zmiennymi statycznymi. Nie może znaleźć pojedynczych wzorców projektowych, zakłada, że ​​wszystkie obiekty można tworzyć wiele razy. Ta wtyczka jest zdecydowanie nieodpowiednia dla frameworków takich jak Spring.
Zombie


6
Chociaż jest to dobra rada, wciąż pytam: „jak przetestować te minimalne obszary, w których wymaganych jest wiele wątków?”
Bryan Rayner

5
„Jeśli jest zbyt skomplikowany, by go przetestować, robisz to źle” - wszyscy musimy zagłębić się w starszy kod, którego nie napisaliśmy. W jaki sposób ta obserwacja pomaga komukolwiek dokładnie?
Ronna,

2
Analiza statyczna jest prawdopodobnie dobrym pomysłem, ale nie jest testowaniem. Ten post naprawdę nie odpowiada na pytanie dotyczące testowania.
Warren Dew,

96

Minęło trochę czasu, kiedy to pytanie zostało opublikowane, ale wciąż nie ma odpowiedzi ...

odpowiedź kleolb02 jest dobra. Spróbuję wejść w więcej szczegółów.

Jest sposób, który ćwiczę dla kodu C #. W przypadku testów jednostkowych powinieneś być w stanie zaprogramować powtarzalne testy, co jest największym wyzwaniem w kodzie wielowątkowym. Tak więc moja odpowiedź ma na celu wymuszenie kodu asynchronicznego na testowej uprzęży, która działa synchronicznie .

Jest to pomysł z książki Gerarda Meszardosa „ Wzorce testowe xUnit ” i nazywa się „Humble Object” (s. 695): Musisz oddzielić podstawowy kod logiczny i wszystko, co pachnie od siebie kodem asynchronicznym. Wynikiem tego byłaby klasa dla podstawowej logiki, która działa synchronicznie .

To pozwala ci na przetestowanie rdzenia logicznego w sposób synchroniczny . Masz absolutną kontrolę nad czasem wykonywania połączeń w oparciu o logikę podstawową, dzięki czemu możesz wykonywać powtarzalne testy. I to jest Twoja korzyść z oddzielenia logiki rdzenia i logiki asynchronicznej.

Ta podstawowa logika musi być otoczona przez inną klasę, która jest odpowiedzialna za asynchroniczne odbieranie wywołań logiki podstawowej i deleguje te wywołania do logiki podstawowej. Kod produkcyjny będzie miał dostęp do podstawowej logiki tylko za pośrednictwem tej klasy. Ponieważ ta klasa powinna jedynie delegować połączenia, jest to bardzo „głupia” klasa bez dużej logiki. Możesz więc utrzymać swoje testy jednostkowe dla tej asychronicznej klasy robotniczej na minimum.

Wszystko powyżej (testowanie interakcji między klasami) to testy składowe. Również w tym przypadku powinieneś mieć całkowitą kontrolę nad czasem, jeśli będziesz trzymać się wzorca „Humble Object”.


1
Ale czasami, jeśli wątki dobrze ze sobą współpracują, to też coś należy przetestować, prawda? Zdecydowanie odseparuję podstawową logikę od części asynchronicznej po przeczytaniu twojej odpowiedzi. Ale nadal będę testować logikę za pomocą interfejsów asynchronicznych z wywołaniem zwrotnym „działaj na wszystkie wątki”.
CopperCash,

Co z systemami wieloprocesorowymi?
Technofil

65

Naprawdę trudny! W moich testach jednostkowych (C ++) podzieliłem to na kilka kategorii zgodnie z zastosowanym wzorem współbieżności:

  1. Testy jednostkowe dla klas, które działają w jednym wątku i nie są świadome wątku - łatwe, przetestuj jak zwykle.

  2. Testy jednostkowe obiektów Monitor (wykonujących zsynchronizowane metody w wątku kontrolnym wywołującego), które ujawniają zsynchronizowany publiczny interfejs API - tworzą wiele próbnych wątków, które wykonują interfejs API. Twórz scenariusze wykorzystujące wewnętrzne warunki obiektu pasywnego. Dołącz jeden dłuższy test, który w zasadzie bije go od wielu wątków przez długi czas. Wiem, że to nienaukowe, ale buduje zaufanie.

  3. Testy jednostkowe dla obiektów aktywnych (tych, które otaczają własny wątek lub wątki kontroli) - podobnie jak w punkcie 2 powyżej, z odmianami zależnymi od projektu klasy. Publiczny interfejs API może blokować lub nie blokować, osoby dzwoniące mogą uzyskać kontrakty terminowe, dane mogą przybywać do kolejek lub wymagać kolejkowania. Możliwych jest tutaj wiele kombinacji; białe pudełko z dala. Nadal wymaga wielu próbnych wątków, aby wywoływać testowany obiekt.

Tak na marginesie:

Podczas wewnętrznych szkoleń dla programistów uczę Pillars of Concurrency i tych dwóch wzorców jako podstawowej struktury myślenia i rozwiązywania problemów związanych z współbieżnością. Istnieją oczywiście bardziej zaawansowane koncepcje, ale odkryłem, że ten zestaw podstaw pomaga inżynierom unikać zupy. Prowadzi to również do kodu, który jest bardziej testowalny jednostkowo, jak opisano powyżej.


51

W ostatnich latach napotkałem ten problem kilka razy, pisząc kod obsługi wątków dla kilku projektów. Podaję spóźnioną odpowiedź, ponieważ większość innych odpowiedzi, podając alternatywy, w rzeczywistości nie odpowiadają na pytanie dotyczące testowania. Moja odpowiedź jest skierowana do przypadków, w których nie ma alternatywy dla kodu wielowątkowego; Zajmuję się projektowaniem kodu pod kątem kompletności, ale także omawiam testy jednostkowe.

Pisanie testowanego wielowątkowego kodu

Pierwszą rzeczą do zrobienia jest oddzielenie kodu obsługi wątku produkcyjnego od całego kodu, który faktycznie przetwarza dane. W ten sposób przetwarzanie danych można przetestować jako kod pojedynczo wątkowy, a jedyną rzeczą, którą robi kod wielowątkowy, jest koordynacja wątków.

Drugą rzeczą do zapamiętania jest to, że błędy w kodzie wielowątkowym są probabilistyczne; błędy, które objawiają się najrzadziej, to błędy, które przekradną się do produkcji, będą trudne do odtworzenia nawet podczas produkcji, a zatem spowodują największe problemy. Z tego powodu standardowe podejście do kodowania polegające na szybkim pisaniu kodu, a następnie debugowaniu go, aż zadziała, jest złym pomysłem na kod wielowątkowy; spowoduje to kod, w którym łatwe błędy zostaną naprawione, a niebezpieczne błędy będą nadal występować.

Zamiast tego, pisząc kod wielowątkowy, musisz napisać kod z takim nastawieniem, że unikniesz pisania błędów. Jeśli poprawnie usunąłeś kod przetwarzania danych, kod obsługi wątku powinien być wystarczająco mały - najlepiej kilka wierszy, w najgorszym przypadku kilkadziesiąt wierszy - abyś miał szansę napisać go bez pisania błędu, a na pewno bez pisania wielu błędów , jeśli rozumiesz wątki, nie spiesz się i zachowaj ostrożność.

Pisanie testów jednostkowych dla kodu wielowątkowego

Po napisaniu kodu wielowątkowego tak ostrożnie, jak to możliwe, nadal warto pisać testy dla tego kodu. Głównym celem testów jest nie tyle testowanie błędów związanych z wyścigiem w zależności od czasu - niemożliwe jest powtarzalne testowanie takich warunków wyścigu - ale raczej sprawdzenie, czy twoja strategia blokowania zapobiegająca takim błędom pozwala na interakcję wielu wątków zgodnie z przeznaczeniem .

Aby poprawnie przetestować prawidłowe działanie blokujące, test musi rozpocząć wiele wątków. Aby test był powtarzalny, chcemy, aby interakcje między wątkami zachodziły w przewidywalnej kolejności. Nie chcemy zewnętrznie synchronizować wątków w teście, ponieważ to maskuje błędy, które mogą się zdarzyć podczas produkcji, w których wątki nie są synchronizowane zewnętrznie. Pozostawia to użycie opóźnień czasowych do synchronizacji wątków, co jest techniką, którą z powodzeniem stosowałem za każdym razem, gdy musiałem pisać testy wielowątkowego kodu.

Jeśli opóźnienia są zbyt krótkie, test staje się kruchy, ponieważ niewielkie różnice czasowe - powiedzmy między różnymi maszynami, na których testy mogą być uruchomione - mogą spowodować, że czas się wyłączy i test się nie powiedzie. To, co zwykle robiłem, to zaczynać od opóźnień, które powodują niepowodzenia testów, zwiększać opóźnienia, aby test przebiegał niezawodnie na mojej maszynie programistycznej, a następnie podwajać opóźnienia poza tym, aby test miał duże szanse na przekazanie na inne maszyny. Oznacza to, że test zajmie makroskopowo dużo czasu, choć z mojego doświadczenia wynika, że ​​staranne zaprojektowanie testu może ograniczyć ten czas do nie więcej niż kilkunastu sekund. Ponieważ nie powinieneś mieć zbyt wielu miejsc wymagających kodu koordynacji wątków w swojej aplikacji, powinno to być dopuszczalne dla twojego zestawu testów.

Na koniec śledź liczbę błędów wykrytych w teście. Jeśli Twój test obejmuje 80% pokrycia kodu, można oczekiwać, że wykryje około 80% twoich błędów. Jeśli Twój test jest dobrze zaprojektowany, ale nie wykrył żadnych błędów, istnieje uzasadniona szansa, że ​​nie masz dodatkowych błędów, które pojawią się tylko podczas produkcji. Jeśli test wykryje jeden lub dwa błędy, nadal możesz mieć szczęście. Poza tym, możesz rozważyć dokładną analizę lub nawet całkowite przepisanie kodu obsługi wątków, ponieważ jest prawdopodobne, że kod nadal zawiera ukryte błędy, które będą bardzo trudne do znalezienia, dopóki kod nie będzie produkowany, i bardzo trudne do naprawienia.


3
Testy mogą jedynie ujawnić obecność błędów, a nie ich brak. Pierwotne pytanie dotyczy problemu 2 wątków, w którym to przypadku wyczerpujące testy mogą być możliwe, ale często tak nie jest. W przypadku czegokolwiek poza najprostszymi scenariuszami może być konieczne ugryzienie kuli i użycie formalnych metod - ale nie pomijaj testów jednostkowych! Pisanie poprawnego wielowątkowego kodu jest przede wszystkim trudne, ale równie trudnym problemem jest zabezpieczenie go przed regresją.
Paul Williams

4
Niesamowite podsumowanie jednego z najmniej zrozumiałych sposobów. Twoja odpowiedź brzmi: prawdziwa segregacja, którą ppl zwykle przeoczają.
prash

1
Kilkanaście sekund to dość długi czas, nawet jeśli masz tylko kilkaset testów o tej długości ...
Toby Speight

1
@TobySpeight Testy są długie w porównaniu do normalnych testów jednostkowych. Przekonałem się, że pół tuzina testów jest więcej niż wystarczających, jeśli kodowanie wątków jest odpowiednio zaprojektowane tak, aby było tak proste, jak to tylko możliwe - wymaganie kilkuset testów wielowątkowych prawie na pewno wskazywałoby na zbyt skomplikowany układ wątków.
Warren Dew

2
To dobry argument za utrzymaniem logiki wątków możliwie jak najbardziej oddzielnej od funkcjonalności (wiem, o wiele łatwiej powiedzieć niż zrobić). I, jeśli to możliwe, rozbicie pakietu testowego na zestawy „każdej zmiany” i „wstępnego zatwierdzenia” (więc nie ma to większego wpływu na twoje minuty na minutę).
Toby Speight

22

Miałem też poważne problemy z testowaniem wielowątkowego kodu. Potem znalazłem naprawdę fajne rozwiązanie w „Wzorcach testowych xUnit” Gerarda Meszarosa. Wzór, który opisuje, nazywa się Humble object .

Zasadniczo opisuje, jak można wyodrębnić logikę do oddzielnego, łatwego do przetestowania komponentu, który jest oddzielony od swojego środowiska. Po przetestowaniu tej logiki można przetestować skomplikowane zachowanie (wielowątkowość, wykonywanie asynchroniczne itp.)


20

Istnieje kilka narzędzi, które są całkiem dobre. Oto podsumowanie niektórych z Java.

Niektóre dobre narzędzia do analizy statycznej to FindBugs (zawiera przydatne wskazówki), JLint , Java Pathfinder (JPF i JPF2) i Bogor .

MultithreadedTC to całkiem dobre narzędzie do analizy dynamicznej (zintegrowane z JUnit), w którym musisz skonfigurować własne przypadki testowe.

ConTest z IBM Research jest interesujący. Instrumentuje Twój kod, wstawiając wszelkiego rodzaju zachowania modyfikujące wątek (np. Uśpienie i wydajność), aby spróbować losowo wykryć błędy.

SPIN to naprawdę fajne narzędzie do modelowania komponentów Java (i innych), ale musisz mieć jakiś użyteczny framework. Jest trudny w użyciu, ale jest niezwykle wydajny, jeśli wiesz, jak go używać. Sporo narzędzi używa SPIN pod maską.

MultithreadedTC jest prawdopodobnie najbardziej popularnym nurtem, ale niektóre z wymienionych powyżej narzędzi analizy statycznej są zdecydowanie warte spojrzenia.


16

Oczekiwanie może być również pomocne w pisaniu deterministycznych testów jednostkowych. Pozwala poczekać, aż jakiś stan w twoim systemie zostanie zaktualizowany. Na przykład:

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

lub

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

Obsługuje również Scala i Groovy.

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

1
Oczekiwanie jest genialne - dokładnie tego szukałem!
Forge_7

14

Innym sposobem (trochę) testowania kodu wątkowego i ogólnie bardzo złożonych systemów jest testowanie Fuzz . To nie jest świetne i nie znajdzie wszystkiego, ale prawdopodobnie będzie przydatne i łatwe do zrobienia.

Zacytować:

Testowanie Fuzz lub Fuzzing to technika testowania oprogramowania, która dostarcza losowe dane („Fuzz”) do wejść programu. Jeśli program zawiedzie (na przykład przez awarię lub awarię asercji wbudowanego kodu), można zauważyć wady. Ogromną zaletą testów Fuzz jest to, że projekt testu jest niezwykle prosty i wolny od uprzedzeń dotyczących zachowania systemu.

...

Testowanie Fuzz jest często stosowane w dużych projektach programistycznych, w których stosuje się testy czarnej skrzynki. Projekty te zwykle mają budżet na opracowanie narzędzi testowych, a testowanie fuzz jest jedną z technik, która oferuje wysoki stosunek korzyści do kosztów.

...

Testowanie Fuzz nie zastępuje jednak wyczerpujących testów ani metod formalnych: może jedynie dostarczyć losową próbkę zachowania systemu, a w wielu przypadkach zdanie testu Fuzz może tylko wykazać, że oprogramowanie obsługuje wyjątki bez awarii, a nie zachowuje się poprawnie. Dlatego testowanie fuzz może być traktowane jedynie jako narzędzie do wykrywania błędów, a nie jako gwarancja jakości.


13

Zrobiłem dużo tego i tak, to do bani.

Kilka porad:

  • GroboUtils do uruchamiania wielu wątków testowych
  • alphaWorks ConTest na klasy instrumentów, aby powodować różne przeplatania między iteracjami
  • Utwórz throwablepole i zaznacz je tearDown(patrz Listing 1). Jeśli złapiesz zły wyjątek w innym wątku, po prostu przypisz go do rzucania.
  • Stworzyłem klasę utils na Listingu 2 i uznałem ją za nieocenioną, szczególnie waitForVerify i waitForCondition, co znacznie zwiększy wydajność twoich testów.
  • Wykorzystaj dobrze AtomicBooleanw swoich testach. Jest bezpieczny dla wątków i często potrzebujesz końcowego typu odwołania do przechowywania wartości z klas wywołań zwrotnych i tym podobnych. Zobacz przykład z listingu 3.
  • Pamiętaj, aby zawsze dać limit czasu swojemu testowi (np. @Test(timeout=60*1000)), Ponieważ testy współbieżności mogą czasami zawieść na zawsze, gdy są zepsute.

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
Limit czasu jest dobrym pomysłem, ale jeśli upłynie limit czasu testu, wszelkie późniejsze wyniki tego przebiegu są podejrzane. Test przekroczenia limitu czasu może nadal uruchamiać niektóre wątki, które mogą Cię zepsuć.
Don Kirkby,

12

Testowanie poprawności kodu MT jest, jak już wspomniano, dość trudnym problemem. Ostatecznie sprowadza się to do zapewnienia, że ​​w twoim kodzie nie ma niepoprawnie zsynchronizowanych wyścigów danych. Problem polega na tym, że istnieje nieskończenie wiele możliwości wykonywania wątków (przeplatania), nad którymi nie masz dużej kontroli (koniecznie przeczytaj ten artykuł). W prostych scenariuszach można faktycznie udowodnić poprawność za pomocą rozumowania, ale zazwyczaj tak nie jest. Zwłaszcza jeśli chcesz uniknąć / zminimalizować synchronizację i nie wybierać najbardziej oczywistej / najłatwiejszej opcji synchronizacji.

Podejście, które stosuję, polega na pisaniu wysoce współbieżnego kodu testowego, aby potencjalnie niewykryte wyścigi danych mogły wystąpić. A potem przez jakiś czas przeprowadzam te testy :) Pewnego razu natknąłem się na rozmowę, w której jakiś informatyk pokazał narzędzie, które to robi (losowo opracowuje test ze specyfikacji, a następnie uruchamia je w sposób szalony, równolegle, sprawdzając określone niezmienniki być zepsutym).

Nawiasem mówiąc, myślę, że ten aspekt testowania kodu MT nie został tutaj wymieniony: zidentyfikuj niezmienniki kodu, które możesz sprawdzić losowo. Niestety znalezienie tych niezmienników jest również dość trudnym problemem. Ponadto mogą nie utrzymywać się przez cały czas podczas wykonywania, więc musisz znaleźć / egzekwować punkty wykonania, w których można oczekiwać, że będą prawdziwe. Doprowadzenie wykonania kodu do takiego stanu jest również trudnym problemem (i może samo w sobie powodować problemy z współbieżnością. Cholera, to cholernie trudne!

Kilka interesujących linków do przeczytania:


autor odnosi się do losowości w testowaniu. Może to być QuickCheck , który został przeniesiony na wiele języków. Możesz obejrzeć dyskusję na temat takich testów dla współbieżnego systemu tutaj
Max

6

Pete Goodliffe ma serię testów jednostkowych kodu wątkowego .

To trudne. Wybieram łatwiejsze wyjście i staram się nie wyodrębniać kodu wątków z rzeczywistego testu. Pete wspomina, że ​​sposób, w jaki to robię, jest zły, ale albo dobrze się rozdzieliłem, albo po prostu miałem szczęście.


6
Przeczytałem dwa dotychczas opublikowane artykuły i nie uważałem ich za bardzo pomocne. Po prostu mówi o trudnościach, nie udzielając wiele konkretnych porad. Może przyszłe artykuły poprawią się.
Don Kirkby,

6

Jeśli chodzi o Javę, sprawdź rozdział 12 JCIP . Istnieje kilka konkretnych przykładów pisania deterministycznych, wielowątkowych testów jednostkowych, aby przynajmniej przetestować poprawność i niezmienniki współbieżnego kodu.

„Sprawdzanie” bezpieczeństwa wątków za pomocą testów jednostkowych jest znacznie trudniejsze. Wierzę, że lepiej temu służą automatyczne testy integracji na różnych platformach / konfiguracjach.


6

Lubię pisać dwie lub więcej metod testowych do wykonania na równoległych wątkach i każda z nich wywołuje testowany obiekt. Używam wywołań Sleep () do koordynowania kolejności wywołań z różnych wątków, ale to nie jest naprawdę niezawodne. Jest również o wiele wolniejszy, ponieważ musisz spać wystarczająco długo, aby czas działał.

Znalazłem bibliotekę wielowątkową TC Java z tej samej grupy, która napisała FindBugs. Pozwala określić kolejność zdarzeń bez użycia Sleep () i jest niezawodny. Jeszcze tego nie próbowałem.

Największym ograniczeniem tego podejścia jest to, że pozwala jedynie przetestować scenariusze, które, jak podejrzewasz, spowodują problemy. Jak powiedzieli inni, naprawdę musisz odizolować swój wielowątkowy kod na niewielką liczbę prostych klas, aby mieć nadzieję na ich dokładne przetestowanie.

Po dokładnym przetestowaniu scenariuszy, które prawdopodobnie spowodują problemy, nienaukowy test, który przez pewien czas wysyła do klasy kilka równoczesnych próśb, jest dobrym sposobem na znalezienie nieoczekiwanych problemów.

Aktualizacja: Grałem trochę z biblioteką Multithreaded TC Java i działa dobrze. Przeniesiłem również niektóre jego funkcje do wersji .NET, którą nazywam TickingTest .


5

Testy jednostkowe elementów gwintowych wykonuję w taki sam sposób, jak w przypadku każdego testu jednostkowego, tj. Z odwróceniem ram kontroli i izolacji. Rozwijam się na arenie .Net i po wyjęciu z pudełka wątki (między innymi) są bardzo trudne (powiedziałbym, że prawie niemożliwe) do pełnej izolacji.

Dlatego napisałem opakowania, które wyglądają mniej więcej tak (uproszczone):

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

Stamtąd mogę łatwo wstrzyknąć program IThreadingManager do moich komponentów i użyć wybranej struktury izolacji, aby wątek zachowywał się zgodnie z oczekiwaniami podczas testu.

To do tej pory działało świetnie dla mnie i używam tego samego podejścia do puli wątków, rzeczy w System.Environment, Sleep itp.


5

Zobacz moją pokrewną odpowiedź na

Projektowanie klasy testowej dla niestandardowej bariery

Jest nastawiony na Javę, ale ma rozsądne podsumowanie opcji.

Podsumowując (IMO), to nie jest użycie jakiegoś fantazyjnego frameworka, który zapewni poprawność, ale to, jak zabierzesz się do projektowania wielowątkowego kodu. Podział obaw (współbieżność i funkcjonalność) to ogromny sposób na zwiększenie zaufania. Rosnące oprogramowanie obiektowe oparte na testach wyjaśnia niektóre opcje lepiej niż ja.

Analiza statyczna i metody formalne (patrz Współbieżność: modele stanowe i programy Java ) są opcją, ale znalazłem, że mają one ograniczone zastosowanie w rozwoju komercyjnym.

Nie zapominaj, że wszelkie testy stylu ładowania / namaczania rzadko gwarantują zwrócenie uwagi na problemy.

Powodzenia!


Powinieneś również wspomnieć o swojej tempus-fugitbibliotece, która helps write and test concurrent code;)
Idolon

4

Niedawno odkryłem (dla Javy) narzędzie o nazwie Threadsafe. Jest to narzędzie do analizy statycznej podobne do findbugs, ale specjalnie do wykrywania problemów z wielowątkowością. Nie zastępuje on testowania, ale mogę go polecić w ramach pisania niezawodnej, wielowątkowej Java.

Łapie nawet niektóre bardzo subtelne potencjalne problemy związane z takimi kwestiami, jak subsumpcja klas, uzyskiwanie dostępu do niebezpiecznych obiektów za pośrednictwem współbieżnych klas i wykrywanie brakujących lotnych modyfikatorów podczas korzystania z podwójnie sprawdzonego paradygmatu blokowania.

Jeśli piszesz wielowątkową Javę, spróbuj .


3

Poniższy artykuł sugeruje 2 rozwiązania. Zawijanie semafora (CountDownLatch) i dodaje takie funkcje, jak eksternalizacja danych z wewnętrznego wątku. Innym sposobem osiągnięcia tego celu jest użycie puli wątków (patrz Użyteczne miejsca).

Zraszacz - zaawansowany obiekt synchronizacji


3
Proszę wyjaśnić podejścia tutaj, linki zewnętrzne mogą być martwe w przyszłości.
Uooo

2

Większość ostatniego tygodnia spędziłem w bibliotece uniwersyteckiej, studiując debugowanie współbieżnego kodu. Głównym problemem jest to, że współbieżny kod nie jest deterministyczny. Zazwyczaj debugowanie akademickie przypada tutaj na jeden z trzech obozów:

  1. Śledzenie zdarzeń / odtwarzanie. Wymaga to monitorowania zdarzeń, a następnie przejrzenia wysłanych zdarzeń. W ramach UT wymagałoby to ręcznego wysłania zdarzeń w ramach testu, a następnie przeprowadzenia pośmiertnych przeglądów.
  2. Skryptowalne. Tutaj wchodzisz w interakcję z działającym kodem za pomocą zestawu wyzwalaczy. „On x> foo, baz ()”. Można to zinterpretować do struktury UT, w której system wykonawczy uruchamia dany test pod pewnymi warunkami.
  3. Interaktywny. To oczywiście nie zadziała w sytuacji automatycznego testowania. ;)

Teraz, jak zauważyli wyżej komentatorzy, możesz zaprojektować swój system współbieżny w bardziej deterministyczny sposób. Jeśli jednak nie zrobisz tego poprawnie, wrócisz do projektowania systemu sekwencyjnego.

Moją sugestią byłoby skoncentrowanie się na bardzo ścisłym protokole projektowym dotyczącym tego, co jest wątkowane, a co nie. Jeśli ograniczysz interfejs tak, aby między elementami były minimalne zależności, jest to o wiele łatwiejsze.

Powodzenia i pracuj nad tym problemem.


2

Miałem niefortunne zadanie testowania kodu wątkowego i są to zdecydowanie najtrudniejsze testy, jakie kiedykolwiek napisałem.

Pisząc testy, użyłem kombinacji delegatów i wydarzeń. Zasadniczo chodzi o używanie PropertyNotifyChangedzdarzeń z jednym WaitCallbacklub kilkoma tego typu ConditionalWaiterankietami.

Nie jestem pewien, czy to było najlepsze podejście, ale mi się udało.


1

Zakładanie kodu „wielowątkowego” oznaczało coś, co jest

  • stanowy i zmienny
  • ORAZ dostęp / modyfikacja przez wiele wątków jednocześnie

Innymi słowy, mówimy o testowaniu niestandardowej, stanowej, bezpiecznej dla wątków klasy / metody / jednostki - która powinna być obecnie bardzo rzadką bestią.

Ponieważ ta bestia jest rzadka, przede wszystkim musimy upewnić się, że istnieją wszystkie uzasadnione wymówki, aby ją napisać.

Krok 1. Rozważ zmianę stanu w tym samym kontekście synchronizacji.

Dzisiaj łatwo jest napisać współbieżny i asynchroniczny kod, w którym operacje we / wy lub inne powolne operacje są odciążone w tle, ale stan współużytkowany jest aktualizowany i sprawdzany w jednym kontekście synchronizacji. np. asynchronizuj / czekaj na zadania i Rx w .NET itp. - wszystkie są testowalne z założenia, „rzeczywiste” zadania i harmonogramy można zastąpić testowaniem deterministycznym (jednak nie wchodzi to w zakres pytania).

To może wydawać się bardzo ograniczone, ale to podejście działa zaskakująco dobrze. Możliwe jest pisanie całych aplikacji w tym stylu bez konieczności zabezpieczania wątków przed jakimkolwiek stanem (tak robię).

Krok 2. Jeśli manipulowanie stanem współdzielonym w kontekście pojedynczej synchronizacji jest absolutnie niemożliwe.

Upewnij się, że koło nie zostało wynalezione na nowo / zdecydowanie nie ma standardowej alternatywy, którą można by dostosować do pracy. Powinno być prawdopodobne, że kod jest bardzo spójny i zawiera się w jednej jednostce, np. Z dużym prawdopodobieństwem jest to specjalny przypadek pewnej standardowej, bezpiecznej dla wątków struktury danych, takiej jak mapa hash lub kolekcja.

Uwaga: jeśli kod jest duży / obejmuje wiele klas ORAZ wymaga manipulacji stanem wielowątkowym, istnieje bardzo duża szansa, że ​​projekt nie jest dobry, ponownie rozważ krok 1

Krok 3. Jeśli ten krok zostanie osiągnięty, musimy przetestować naszą własną, stanową, bezpieczną dla wątków klasę / metodę / jednostkę .

Będę śmiertelnie szczery: nigdy nie musiałem pisać odpowiednich testów dla takiego kodu. Przez większość czasu uciekam w kroku 1, czasem w kroku 2. Ostatnim razem, gdy musiałem napisać niestandardowy kod bezpieczny dla wątków, było tak wiele lat temu, że to było przed przyjęciem testowania jednostkowego / prawdopodobnie nie musiałbym go pisać i tak z obecną wiedzą.

Gdybym naprawdę musiał przetestować taki kod ( wreszcie rzeczywista odpowiedź ), spróbowałbym kilku rzeczy poniżej

  1. Niedeterministyczne testy warunków skrajnych. np. uruchom 100 wątków jednocześnie i sprawdź, czy wynik końcowy jest spójny. Jest to bardziej typowe w przypadku testów na wyższym poziomie / w testach integracyjnych wielu użytkowników, ale można je również stosować na poziomie jednostki.

  2. Odsłoń niektóre „zaczepy” testowe, w których test może wstrzyknąć kod, aby pomóc w tworzeniu deterministycznych scenariuszy, w których jeden wątek musi wykonać operację przed drugim. Choć jest tak brzydka, nie mogę wymyślić nic lepszego.

  3. Testy oparte na opóźnieniu w celu uruchomienia wątków i wykonywania operacji w określonej kolejności. Ściśle mówiąc, takie testy również nie są deterministyczne (istnieje możliwość zatrzymania / zatrzymania systemu GC w kolekcji, co może zniekształcić opóźnienie w innym przypadku), jest również brzydkie, ale pozwala uniknąć haków.


0

Do kodu J2E użyłem SilkPerformer, LoadRunner i JMeter do testowania współbieżności wątków. Wszyscy robią to samo. Zasadniczo zapewniają one stosunkowo prosty interfejs do administrowania ich wersją serwera proxy, niezbędny do analizy strumienia danych TCP / IP i symulacji wielu użytkowników wysyłających jednocześnie żądania do serwera aplikacji. Serwer proxy może dać ci możliwość robienia takich rzeczy, jak analizowanie złożonych żądań, prezentując całą stronę i adres URL wysłany do serwera, a także odpowiedź z serwera, po przetworzeniu żądania.

Niektóre błędy można znaleźć w niepewnym trybie http, w którym można przynajmniej analizować przesyłane dane formularzy i systematycznie je zmieniać dla każdego użytkownika. Ale prawdziwe testy są przeprowadzane w trybie https (Secured Socket Layers). Następnie musisz również zmagać się z systematyczną zmianą danych sesji i plików cookie, które mogą być nieco bardziej skomplikowane.

Najlepszy błąd, jaki kiedykolwiek znalazłem, podczas testowania współbieżności, polegał na tym, że odkryłem, że programista polegał na zbieraniu pamięci Java w celu zamknięcia żądania połączenia ustanowionego podczas logowania do serwera LDAP podczas logowania. Spowodowało to ujawnienie użytkowników do sesji innych użytkowników i bardzo mylących wyników, gdy próbujesz przeanalizować, co się stało, gdy serwer upadł na kolana, ledwo co sfinalizując jedną transakcję, co kilka sekund.

W końcu ty lub ktoś prawdopodobnie będziesz musiał zapiąć pasy i przeanalizować kod pod kątem błędów, takich jak ten, o którym właśnie wspomniałem. Najbardziej użyteczna jest otwarta dyskusja między działami, taka jak ta, która miała miejsce, kiedy rozwinęliśmy opisany powyżej problem. Ale te narzędzia są najlepszym rozwiązaniem do testowania wielowątkowego kodu. JMeter jest oprogramowaniem typu open source. SilkPerformer i LoadRunner są zastrzeżone. Jeśli naprawdę chcesz wiedzieć, czy Twoja aplikacja jest bezpieczna dla wątków, tak robią to duzi chłopcy. Zrobiłem to dla bardzo dużych firm profesjonalnie, więc nie zgaduję. Mówię z własnego doświadczenia.

Słowo ostrzeżenia: zrozumienie tych narzędzi zajmuje trochę czasu. Nie będzie to po prostu instalacja oprogramowania i uruchamianie GUI, chyba że masz już styczność z programowaniem wielowątkowym. Próbowałem zidentyfikować 3 kluczowe kategorie obszarów do zrozumienia (formularze, dane sesji i pliki cookie), mając nadzieję, że przynajmniej rozpoczęcie od zrozumienia tych tematów pomoże Ci skoncentrować się na szybkich wynikach, zamiast konieczności zapoznania się z cała dokumentacja.


0

Współbieżność to złożona gra między modelem pamięci, sprzętem, pamięciami podręcznymi i naszym kodem. W przypadku Javy przynajmniej takie testy zostały częściowo rozwiązane głównie przez jcstress . Twórcy tej biblioteki są znani jako autorzy wielu funkcji współbieżności JVM, GC i Java.

Ale nawet ta biblioteka wymaga dobrej znajomości specyfikacji Java Memory Model, abyśmy dokładnie wiedzieli, co testujemy. Myślę jednak, że celem tego wysiłku są znaki mircobenchmark. Niezbyt duże aplikacje biznesowe.


0

Jest artykuł na ten temat, używając Rust jako języka w przykładowym kodzie:

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

Podsumowując, sztuczka polega na napisaniu logiki współbieżnej tak, aby była odporna na niedeterminizm związany z wieloma wątkami wykonania, przy użyciu narzędzi takich jak kanały i zmienne.

Następnie, jeśli tak ustrukturyzowałeś swoje „komponenty”, najłatwiejszym sposobem przetestowania ich jest użycie kanałów do wysłania do nich wiadomości, a następnie zablokowanie innych kanałów w celu potwierdzenia, że ​​komponent wysyła określone oczekiwane wiadomości.

Link do artykułu jest w pełni napisany przy użyciu testów jednostkowych.


-1

Jeśli testujesz prosty nowy wątek (runnable) .run () Możesz wyśmiewać Thread, aby sekwencyjnie uruchamiać runnable

Na przykład, jeśli kod testowanego obiektu wywołuje taki nowy wątek

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

Następnie pomocne może być wykpiwanie nowych wątków i sekwencyjne uruchamianie argumentu uruchamialnego

@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

(jeśli to możliwe) nie używaj wątków, używaj aktorów / aktywnych obiektów. Łatwy do przetestowania.


2
@OMTheEternity może, ale to wciąż najlepsza odpowiedź imo.
Dill

-5

Możesz użyć EasyMock.makeThreadSafe, aby instancja testowa była bezpieczna dla wątków


To wcale nie jest możliwy sposób testowania wielowątkowego kodu. Problemem nie jest to, że kod testowy działa wielowątkowo, ale że testujesz kod, który zwykle działa wielowątkowo. I nie możesz zsynchronizować wszystkiego, ponieważ wtedy nie testujesz już wyścigów danych.
bennidi
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.