How should I unit test threaded code?

后端 未结 26 1344
悲&欢浪女
悲&欢浪女 2020-11-22 04:09

I have thus far avoided the nightmare that is testing multi-threaded code since it just seems like too much of a minefield. I\'d like to ask how people have gone about test

26条回答
  •  栀梦
    栀梦 (楼主)
    2020-11-22 04:51

    I've done a lot of this, and yes it sucks.

    Some tips:

    • GroboUtils for running multiple test threads
    • alphaWorks ConTest to instrument classes to cause interleavings to vary between iterations
    • Create a throwable field and check it in tearDown (see Listing 1). If you catch a bad exception in another thread, just assign it to throwable.
    • I created the utils class in Listing 2 and have found it invaluable, especially waitForVerify and waitForCondition, which will greatly increase the performance of your tests.
    • Make good use of AtomicBoolean in your tests. It is thread safe, and you'll often need a final reference type to store values from callback classes and suchlike. See example in Listing 3.
    • Make sure to always give your test a timeout (e.g., @Test(timeout=60*1000)), as concurrency tests can sometimes hang forever when they're broken.

    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 null
     * 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. null 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 true 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());
    }
    

提交回复
热议问题