Как мне выполнить модульное тестирование многопоточного кода? - PullRequest
643 голосов
/ 15 августа 2008

До сих пор я избегал кошмара, в котором тестируется многопоточный код, поскольку он кажется слишком большим минным полем. Я хотел бы спросить, как люди занимались тестированием кода, который опирается на потоки для успешного выполнения, или просто как люди занимались тестированием таких проблем, которые проявляются только тогда, когда два потока взаимодействуют определенным образом?

Это кажется действительно ключевой проблемой для программистов сегодня, было бы полезно объединить наши знания по этому вопросу.

Ответы [ 25 ]

227 голосов
/ 15 августа 2008

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

Самый простой способ протестировать сложный, многопоточный код приложения: если он слишком сложен для тестирования, вы делаете это неправильно. Если у вас есть один экземпляр с несколькими потоками, действующими на него, и вы не можете проверить ситуации, когда эти потоки пересекаются друг с другом, то ваш дизайн необходимо переделать. Это так же просто и так сложно.

Существует много способов программирования многопоточности, позволяющих избежать одновременного выполнения потоков через экземпляры. Самое простое - сделать все ваши объекты неизменяемыми. Конечно, это обычно не возможно. Таким образом, вы должны определить те места в вашем проекте, где потоки взаимодействуют с одним и тем же экземпляром, и уменьшить количество этих мест. Делая это, вы изолируете несколько классов, где на самом деле происходит многопоточность, что снижает общую сложность тестирования вашей системы.

Но вы должны понимать, что даже делая это, вы все равно не можете проверить каждую ситуацию, когда два потока наступают друг на друга. Для этого вам нужно будет одновременно запустить два потока в одном и том же тесте, а затем точно контролировать, какие строки они выполняют в данный момент. Лучшее, что вы можете сделать, это смоделировать эту ситуацию. Но это может потребовать от вас написания кода специально для тестирования, и в лучшем случае это полшага к настоящему решению.

Вероятно, лучший способ проверить код на наличие потоков - это статический анализ кода. Если ваш многопоточный код не соответствует конечному набору потоковых шаблонов, возможно, у вас проблема. Я считаю, что Code Analysis в VS содержит некоторые знания о многопоточности, но, вероятно, не так много.

Посмотрите, как обстоят дела в настоящее время (и, вероятно, наступит хорошее время), лучший способ протестировать многопоточные приложения - максимально снизить сложность многопоточного кода. Минимизируйте области, в которых взаимодействуют потоки, тестируйте как можно лучше и используйте анализ кода для выявления опасных областей.

87 голосов
/ 18 января 2009

Прошло много времени, когда этот вопрос был опубликован, но на него все еще не ответили ...

kleolb02 хороший ответ. Я постараюсь вдаваться в подробности.

Есть способ, который я практикую для кода C #. Для модульных тестов вы должны быть в состоянии запрограммировать воспроизводимые тесты, что является самой большой проблемой в многопоточном коде. Таким образом, мой ответ направлен на то, чтобы заставить асинхронный код использовать тестовую систему, которая работает синхронно .

Это идея из книги Джерарда Месардоса " xUnit Test Patterns ", и она называется "Смиренный объект" (стр. 695): вам нужно отделить основной логический код и все, что пахнет как асинхронный код от каждого Другой. Это привело бы к классу для базовой логики, который работает синхронно .

Это позволяет вам тестировать основной логический код синхронно 1020 *. Вы имеете абсолютный контроль над временем вызовов, которые вы делаете на логике ядра, и, таким образом, можете сделать воспроизводимых тестов. И это ваша выгода от разделения базовой логики и асинхронной логики.

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

Все, что выше (тестирование взаимодействия между классами), является тестированием компонентов. Также в этом случае вы сможете иметь абсолютный контроль над временем, если будете придерживаться шаблона «Смиренный объект».

58 голосов
/ 15 августа 2008

Действительно сложный! В моих (C ++) модульных тестах я разбил это на несколько категорий в соответствии с используемым шаблоном параллелизма:

  1. Модульные тесты для классов, которые работают в одном потоке и не поддерживают потоки - просто, тестируйте как обычно.

  2. Модульные тесты для Мониторинг объектов (те, которые выполняют синхронизированные методы в потоке контроля вызывающих), которые предоставляют синхронизированный общедоступный API - создание нескольких фиктивных потоков, которые осуществляют API. Построить сценарии, которые осуществляют внутренние условия пассивного объекта. Включите один более длительный тест, который в основном выбивает его из нескольких потоков в течение длительного периода времени. Я знаю, что это ненаучно, но это вселяет уверенность.

  3. Модульные тесты для Активные объекты (те, которые инкапсулируют свой собственный поток или потоки управления) - аналогично # 2 выше с вариациями в зависимости от дизайна класса. Публичный API может быть блокирующим или неблокирующим, вызывающие абоненты могут получать фьючерсы, данные могут поступать в очереди или должны быть исключены из очереди. Здесь возможно множество комбинаций; белая коробка прочь Для выполнения вызовов тестируемого объекта по-прежнему требуется несколько фиктивных потоков.

В сторону:

Во внутреннем обучении разработчиков, которым я занимаюсь, я преподаю Столпы параллелизма и эти два паттерна в качестве основной основы для размышлений и разложения проблем параллелизма. Очевидно, есть более продвинутые концепции, но я обнаружил, что этот набор основ помогает не допустить появления инженеров. Это также приводит к тому, что код будет более тестируемым, как описано выше.

43 голосов
/ 12 сентября 2015

В последние годы я сталкивался с этой проблемой несколько раз, когда писал код обработки потоков для нескольких проектов. Я даю поздний ответ, потому что большинство других ответов, хотя и предоставляют альтернативы, на самом деле не отвечают на вопрос о тестировании. Мой ответ адресован случаям, когда нет альтернативы многопоточному коду; Я рассматриваю вопросы разработки кода для полноты, но также обсуждаю модульное тестирование.

Написание тестируемого многопоточного кода

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

Второе, что нужно помнить, это то, что ошибки в многопоточном коде вероятностны; ошибки, которые проявляются реже всего, - это ошибки, которые проникнут в производство, их будет трудно воспроизвести даже в процессе производства и, следовательно, они вызовут самые большие проблемы. По этой причине стандартный подход к кодированию - быстро написать код, а затем отладить его, пока он не заработает, - плохая идея для многопоточного кода; это приведет к коду, в котором исправлены простые ошибки, а опасные ошибки все еще существуют.

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

Написание модульных тестов для многопоточного кода

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

Чтобы правильно проверить правильность поведения блокировки, тест должен запустить несколько потоков. Чтобы сделать тест повторяемым, мы хотим, чтобы взаимодействия между потоками происходили в предсказуемом порядке. Мы не хотим внешнюю синхронизацию потоков в тесте, потому что это замаскирует ошибки, которые могут произойти в производственном процессе, когда потоки не синхронизированы извне. Это оставляет использование временных задержек для синхронизации потоков, что является техникой, которую я успешно использовал всякий раз, когда мне приходилось писать тесты многопоточного кода.

Если задержки слишком короткие, то тест становится хрупким, потому что незначительные различия во времени - скажем, между разными машинами, на которых могут выполняться тесты - могут привести к отключению синхронизации и провалу теста. Обычно я начинаю с задержек, которые приводят к сбоям теста, увеличивают задержки, чтобы тест надежно проходил на моей машине для разработки, а затем удваивают задержки, чтобы тест имел хорошие шансы на прохождение других машин. Это означает, что тест займет макроскопическое количество времени, хотя, по моему опыту, тщательная разработка теста может ограничить это время не более чем дюжиной секунд. Поскольку в вашем приложении не должно быть много мест, требующих кода координации потоков, это должно быть приемлемо для вашего набора тестов.

Наконец, следите за количеством ошибок, обнаруженных вашим тестом. Если ваш тест покрывает 80% кода, можно ожидать, что он поймает около 80% ваших ошибок. Если ваш тест хорошо спроектирован, но не обнаружил ошибок, есть разумный шанс, что у вас не будет дополнительных ошибок, которые будут отображаться только в рабочей среде. Если тест обнаружит одну или две ошибки, вам все равно может повезти. Помимо этого, и вы можете рассмотреть возможность тщательного анализа или даже полного переписывания кода обработки потоков, поскольку вполне вероятно, что код все еще содержит скрытые ошибки, которые будет очень трудно найти, пока код не будет запущен, и очень трудно исправить тогда.

21 голосов
/ 20 августа 2008

У меня также были серьезные проблемы с тестированием многопоточного кода. Тогда я нашел действительно классное решение в «Тестовых шаблонах xUnit» Джерарда Месароса. Образец, который он описывает, называется Смиренный объект .

В основном, это описывает, как вы можете извлечь логику в отдельный, легко тестируемый компонент, который отделен от его среды. После проверки этой логики вы можете проверить сложное поведение (многопоточность, асинхронное выполнение и т. Д.)

17 голосов
/ 08 июля 2010

Есть несколько хороших инструментов. Вот краткое изложение некоторых из Java.

Некоторые хорошие инструменты статического анализа включают FindBugs (дает некоторые полезные советы), JLint , Java Pathfinder (JPF & JPF2) и Bogor .

MultithreadedTC - неплохой инструмент динамического анализа (встроенный в JUnit), в котором вам нужно настроить собственные тестовые случаи.

ConTest от IBM Research - это интересно. Он обрабатывает ваш код, вставляя все виды изменяющих поток действий (например, сон и выход), чтобы попытаться случайно обнаружить ошибки.

SPIN - это действительно классный инструмент для моделирования ваших Java (и других) компонентов, но вам нужно иметь некоторую полезную среду. Трудно использовать как есть, но чрезвычайно мощный, если вы знаете, как его использовать. Многие инструменты используют SPIN под капотом.

MultithreadedTC, вероятно, является наиболее распространенным, но некоторые из перечисленных выше инструментов статического анализа, безусловно, стоит посмотреть.

13 голосов
/ 24 сентября 2008

Другой способ (своего рода) тестирования многопоточного кода и очень сложных систем в целом - через Fuzz Testing . Это не здорово, и он не найдет все, но он, вероятно, будет полезен и прост в выполнении.

Цитата:

Нечеткое тестирование или фаззинг - это метод тестирования программного обеспечения, который предоставляет случайные данные («фазз») на входы программы. В случае сбоя программы (например, из-за сбоя или из-за ошибок во встроенном коде), дефекты могут быть отмечены. Большим преимуществом нечеткого тестирования является то, что дизайн теста чрезвычайно прост и не содержит предвзятых мнений о поведении системы.

...

Нечеткое тестирование часто используется в крупных проектах по разработке программного обеспечения, в которых используется тестирование черного ящика. Эти проекты, как правило, имеют бюджет на разработку инструментов тестирования, и нечеткое тестирование является одним из методов, который предлагает высокое соотношение выгод и затрат.

...

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

12 голосов
/ 19 ноября 2010

Awaitility также может быть полезен для написания детерминированных модульных тестов. Это позволяет вам ждать, пока какое-то состояние в вашей системе не будет обновлено. Например:

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

или

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

Также имеется поддержка Scala и Groovy.

await until { something() > 4 } // Scala example
11 голосов
/ 18 сентября 2014

Проверка МТ-кода на корректность, как уже говорилось, является довольно сложной задачей. В итоге все сводится к тому, чтобы в вашем коде не было неправильно синхронизированных гонок данных. Проблема в том, что существует бесконечно много возможностей выполнения потоков (чередования), над которыми у вас нет особого контроля (хотя обязательно прочитайте эту статью). В простых сценариях можно было бы реально доказать правильность, рассуждая, но обычно это не так. Особенно, если вы хотите избежать / минимизировать синхронизацию и не использовать самый очевидный / самый простой вариант синхронизации.

Подход, которым я придерживаюсь, заключается в написании высококонкурентного тестового кода для вероятности возникновения потенциально необнаруженных гонок данных. А потом я в течение некоторого времени запускал эти тесты :) Однажды я наткнулся на разговор, в котором какой-то ученый демонстрирует инструмент, который это делает (случайным образом разрабатывает тесты из спецификаций, а затем дико, параллельно их прогоняет, проверяя определенные инварианты). быть сломанным).

Кстати, я думаю, что этот аспект тестирования кода MT здесь не упоминался: выявляйте инварианты кода, которые вы можете проверить случайным образом. К сожалению, найти эти инварианты тоже довольно сложно. Кроме того, они могут не удерживаться все время во время выполнения, поэтому вы должны найти / применить точки выполнения, где вы можете ожидать, что они будут истинными. Приведение выполнения кода в такое состояние также является сложной проблемой (и может само по себе вызывать проблемы с параллелизмом. Вот так, чертовски сложно!

Некоторые интересные ссылки для чтения:

11 голосов
/ 24 сентября 2008

Я многое сделал, и да, это отстой.

Несколько советов:

  • GroboUtils для запуска нескольких тестовых потоков
  • alphaWorks ConTest для классов инструментов, чтобы чередование чередования между итерациями
  • Создайте поле throwable и отметьте его в tearDown (см. Листинг 1). Если вы поймали плохое исключение в другом потоке, просто назначьте его на throwable.
  • Я создал класс utils в листинге 2 и нашел его бесценным, особенно 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());
}
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...