Статьи

Новый способ протестировать ваш многопоточный код с помощью JUnit

Далее вы увидите простой пример того, как протестировать многопоточный код Java с помощью JUnit . Предположим, мы хотим создать счетчик, который можно использовать одновременно. Итак, мы начнем с класса Counter и JUnit test TestCounter:

public class Counter {

    private int count=0;

    public void addOne()
    {
        count++;
    }

    public int getCount()
    {
        return count;
    }    
}
import static org.junit.Assert.assertEquals;
import org.junit.After;
import org.junit.Test;
import org.junit.runner.RunWith;
import com.anarsoft.vmlens.concurrent.junit.ConcurrentTestRunner;

@RunWith(ConcurrentTestRunner.class)
public class TestCounter {

    private Counter counter = new Counter();

    @Test
    public void addOne()
    {
        counter.addOne();
    }

    @After
    public void testCount()
    {
        assertEquals("4 Threads running addOne in parallel should lead to 4" , 4 , counter);
    }

}

Используя аннотацию RunWith, тест JUnit запускается специальным ConcurrentTestRunner . Этот тестовый прогон запускает метод, аннотированный «Test», параллельным в 4 потоках. После этого он выполняет методы, помеченные аннотацией «После» в основном потоке.
течь
Если мы запустим тестовый случай с помощью средства отслеживания состояния гонки, такого как vmlens, мы увидим следующее:
race_condition_unit_test_counter
У нас есть условие гонки, которое получает доступ к количеству полей. Чтобы решить эту проблему, мы объявляем count как volatile и запускаем тест снова.

private volatile int count=0;

Теперь контрольный пример успешен. По крайней мере, почти все время. Если вы запускаете тестовый пример очень часто, вы иногда увидите исключение из проверки. Чтобы увидеть, что происходит, мы запускаем его с «Задержкой синхронизации для модульных тестов» в vmlens .

Теперь вы всегда увидите следующее исключение:

java.lang.AssertionError: 4 Threads running addOne in parallel should lead to 4 expected:<4> but was:<3>
    at org.junit.Assert.fail(Assert.java:88)
    at org.junit.Assert.failNotEquals(Assert.java:834)
    at org.junit.Assert.assertEquals(Assert.java:645)
    at TestCounter.testCount(TestCounter.java:21)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(Unknown Source)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(Unknown Source)
    at java.lang.reflect.Method.invoke(Unknown Source)
    at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:50)
    at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)
    at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:47)
    at com.anarsoft.vmlens.concurrent.junit.internal.InvokeListOfMethods.evaluate(InvokeListOfMethods.java:23)
    at com.anarsoft.vmlens.concurrent.junit.internal.ConcurrentStatement.evaluateStatement(ConcurrentStatement.java:12)
    at com.anarsoft.vmlens.concurrent.junit.ConcurrentTestRunner.evaluateStatement(ConcurrentTestRunner.java:212)
    at com.anarsoft.vmlens.concurrent.junit.ConcurrentTestRunner.runChildrenConcurrently(ConcurrentTestRunner.java:172)
    at com.anarsoft.vmlens.concurrent.junit.ConcurrentTestRunner.access$0(ConcurrentTestRunner.java:78)
    at com.anarsoft.vmlens.concurrent.junit.ConcurrentTestRunner$1.evaluate(ConcurrentTestRunner.java:72)
    at org.junit.runners.ParentRunner.run(ParentRunner.java:363)
    at org.eclipse.jdt.internal.junit4.runner.JUnit4TestReference.run(JUnit4TestReference.java:86)
    at org.eclipse.jdt.internal.junit.runner.TestExecution.run(TestExecution.java:38)
    at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.java:459)
    at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.java:675)
    at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.run(RemoteTestRunner.java:382)
    at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.main(RemoteTestRunner.java:192)

На самом деле count ++ — это не одна операция, а операции с 6-байтовым кодом, содержащие одно чтение и одну запись в поле count:

ALOAD 0: this
DUP
GETFIELD Counter.count : int
ICONST_1
IADD
PUTFIELD Counter.count : int

Вводя задержку между этими тремя операциями, мы гарантируем, что два потока выполняют эти операции параллельно. При параллельном выполнении счет всегда будет меньше 4, иногда 3, иногда только 2.

Чтобы решить эту проблему, мы должны сделать эти методы атомарными. Это можно сделать с помощью java.util.concurrent.atomic.AtomicInteger:

import java.util.concurrent.atomic.AtomicInteger;

public class Counter {

    private final AtomicInteger  count= new AtomicInteger();

    public void addOne()
    {
        count.incrementAndGet();
    }

    public int getCount()
    {
        return count.get();
    }    
}

Теперь контрольный пример всегда проходит успешно. В качестве тестового бегуна я использовал concurrent-junit , а в качестве ловушки условий гонки я использовал vmlens

И хотя это довольно простой метод, я понимаю, что есть некоторые сложные моменты. Особенно, когда вам нужно тестировать более сложные классы. Так что, если у вас есть какие-либо вопросы, просто задайте их в комментарии ниже.