How should I unit test threaded code?

后端 未结 26 1349
悲&欢浪女
悲&欢浪女 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:48

    For Java, check out chapter 12 of JCIP. There are some concrete examples of writing deterministic, multi-threaded unit tests to at least test the correctness and invariants of concurrent code.

    "Proving" thread-safety with unit tests is much dicier. My belief is that this is better served by automated integration testing on a variety of platforms/configurations.

    0 讨论(0)
  • 2020-11-22 04:48

    There is an article on the topic, using Rust as the language in the example code:

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

    In summary, the trick is to write your concurrent logic so that it is robust to the non-determinism involved with multiple threads of execution, using tools like channels and condvars.

    Then, if that is how you've structured your "components", the easiest way to test them is by using channels to send messages to them, and then block on other channels to assert that the component sends certain expected messages.

    The linked-to article is fully written using unit-tests.

    0 讨论(0)
  • 2020-11-22 04:49

    You may use EasyMock.makeThreadSafe to make testing instance threadsafe

    0 讨论(0)
  • 2020-11-22 04:51

    Tough one indeed! In my (C++) unit tests, I've broken this down into several categories along the lines of the concurrency pattern used:

    1. Unit tests for classes that operate in a single thread and aren't thread aware -- easy, test as usual.

    2. Unit tests for Monitor objects (those that execute synchronized methods in the callers' thread of control) that expose a synchronized public API -- instantiate multiple mock threads that exercise the API. Construct scenarios that exercise internal conditions of the passive object. Include one longer running test that basically beats the heck out of it from multiple threads for a long period of time. This is unscientific I know but it does build confidence.

    3. Unit tests for Active objects (those that encapsulate their own thread or threads of control) -- similar to #2 above with variations depending on the class design. Public API may be blocking or non-blocking, callers may obtain futures, data may arrive at queues or need to be dequeued. There are many combinations possible here; white box away. Still requires multiple mock threads to make calls to the object under test.

    As an aside:

    In internal developer training that I do, I teach the Pillars of Concurrency and these two patterns as the primary framework for thinking about and decomposing concurrency problems. There's obviously more advanced concepts out there but I've found that this set of basics helps keep engineers out of the soup. It also leads to code that is more unit testable, as described above.

    0 讨论(0)
  • 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 <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());
    }
    
    0 讨论(0)
  • 2020-11-22 04:51

    If you are testing simple new Thread(runnable).run() You can mock Thread to run the runnable sequentially

    For instance, if the code of the tested object invokes a new thread like this

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

    Then mocking new Threads and run the runnable argument sequentially can help

    @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 
    }
    
    0 讨论(0)
提交回复
热议问题