我應該如何對線程代碼進行單元測試?

到目前爲止,我彷佛避免了測試多線程代碼的噩夢,由於它彷佛太多了。 我想問一下人們如何去測試依賴於線程的代碼才能成功執行,或者人們如何去測試那些僅在兩個線程以給定方式交互時纔會出現的問題? java

對於當今的程序員來講,這彷佛是一個很是關鍵的問題,將咱們的知識集中在這一恕我直言中將頗有用。 git


#1樓

我曾經承擔過測試線程代碼的不幸任務,它們絕對是我編寫過的最難的測試。 程序員

在編寫測試時,我結合使用了委託和事件。 基本上,全部有關將PropertyNotifyChanged事件與WaitCallback或某種輪詢的ConditionalWaiter使用。 apache

我不肯定這是不是最好的方法,可是它對我來講是可行的。 編程


#2樓

確實很強悍! 在個人(C ++)單元測試中,按照使用的併發模式將其分爲幾類: api

  1. 對在單個線程中運行且不瞭解線程的類進行單元測試-輕鬆進行常規測試。 安全

  2. 暴露公開的公共API的Monitor對象 (在調用者的控制線程中執行同步方法的對象)的單元測試-實例化使用該API的多個模擬線程。 構造適用於被動對象內部條件的方案。 包括一個運行時間更長的測試,該測試基本上能夠長時間消除來自多個線程的麻煩。 我知道這是不科學的,但確實創建了信心。 多線程

  3. Active對象 (封裝了本身的一個或多個控制線程的對象)的單元測試-與上面的#2類似,具體取決於類設計。 公共API可能處於阻塞狀態或非阻塞狀態,調用者可能會獲取期貨,數據可能會到達隊列或須要出隊。 這裏有不少組合。 白盒子走了。 仍然須要多個模擬線程來調用被測對象。 併發

做爲旁白: 框架

在我進行的內部開發人員培訓中,我講授併發支柱和這兩種模式做爲思考和分解併發問題的主要框架。 顯然還有更高級的概念,可是我發現這套基礎知識有助於使工程師遠離困境。 如上所述,它還會致使代碼更可單元測試。


#3樓

看,沒有簡單的方法能夠作到這一點。 我正在開發一個本質上是多線程的項目。 事件來自操做系統,我必須同時處理它們。

處理複雜的多線程應用程序代碼的最簡單方法是:若是過於複雜而沒法測試,則說明您作錯了。 若是您有一個實例,該實例具備做用於其上的多個線程,而且您沒法測試這些線程彼此間步調一致的狀況,則須要從新設計。 它既簡單又複雜。

爲多線程編程的方法有不少,能夠避免線程同時在實例中運行。 最簡單的是使全部對象不可變。 固然,一般是不可能的。 所以,您必須肯定設計中線程與同一實例交互的那些位置,並減小這些位置的數量。 經過這樣作,您隔離了實際發生多線程的幾個類,從而下降了測試系統的整體複雜性。

可是您必須意識到,即便這樣作,您仍然沒法測試兩個線程相互踩踏的全部狀況。 爲此,您必須在同一測試中同時運行兩個線程,而後精確控制它們在任何給定時刻執行的行。 您能作的最好的就是模擬這種狀況。 但這可能須要您專門爲測試編寫代碼,而這僅是邁向真正解決方案的一半。

測試代碼是否存在線程問題的最佳方法多是對代碼進行靜態分析。 若是您的線程代碼沒有遵循一組有限的線程安全模式,那麼您可能會遇到問題。 我相信VS中的代碼分析確實包含一些線程知識,但可能很少。

看起來,就目前而言(可能會好時機),測試多線程應用程序的最佳方法是儘量下降線程代碼的複雜性。 儘可能減小線程交互的區域,儘量地進行測試,並使用代碼分析來識別危險區域。


#4樓

我已經作了不少,是的。

一些技巧:

  • GroboUtils用於運行多個測試線程
  • alphaWorks ConTest能夠檢測類,以使插入在迭代之間有所不一樣
  • 建立一個throwable字段,並在tearDown對其進行檢查(參見清單1)。 若是您在另外一個線程中捕獲到嚴重異常,只需將其分配給throwable。
  • 我在清單2中建立了utils類,並發現它很是有價值,尤爲是waitForVerify和waitForCondition,它們將大大提升測試的性能。
  • 在測試中充分利用AtomicBoolean 。 它是線程安全的,而且您一般須要最終引用類型來存儲回調類之類的值。 參見清單3中的示例。
  • 確保始終給您的測試超時(例如@Test(timeout=60*1000) ),由於併發測試有時會在中斷時永久掛起

清單1:

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

清單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;
    }
}
}

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

#5樓

測試(一般)測試線程代碼(一般是很是複雜的系統)的另外一種方法是經過Fuzz Testing 。 它不是很好,而且沒法找到全部內容,可是它可能頗有用且操做簡單。

引用:

模糊測試或模糊測試是一種軟件測試技術,可爲程序的輸入提供隨機數據(「模糊」)。 若是程序失敗(例如,因爲崩潰或內置代碼斷言失敗),則能夠指出缺陷。 模糊測試的最大優勢是測試設計很是簡單,而且沒有對系統行爲的先入之見。

...

模糊測試一般用於採用黑匣子測試的大型軟件開發項目中。 這些項目一般有預算來開發測試工具,而模糊測試是提供高性價比的技術之一。

...

可是,模糊測試不能替代詳盡的測試或形式化方法:它只能提供系統行爲的隨機樣本,而且在許多狀況下,經過模糊測試可能僅代表某軟件能夠處理異常而不會崩潰,而不是行爲正確。 所以,模糊測試只能被視爲發現錯誤的工具,而不能保證質量。

相關文章
相關標籤/搜索