Как мне выполнить модульный тест многопоточного кода?

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

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

30 ответов

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

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

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

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

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

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

Это было время, когда этот вопрос был опубликован, но до сих пор не ответили...

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

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

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

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

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

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

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

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

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

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

Как в сторону:

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Цитата:

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

...

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

...

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

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

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

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

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

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

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

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

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

Поэтому я написал обертки, которые выглядят примерно так (упрощенно):

public interface IThread
{
    void Start();
    ...
}

public class ThreadWrapper : IThread
{
    private readonly Thread _thread;

    public ThreadWrapper(ThreadStart threadStart)
    {
        _thread = new Thread(threadStart);
    }

    public Start()
    {
        _thread.Start();
    }
}

public interface IThreadingManager
{
    IThread CreateThread(ThreadStart threadStart);
}

public class ThreadingManager : IThreadingManager
{
    public IThread CreateThread(ThreadStart threadStart)
    {
         return new ThreadWrapper(threadStart)
    }
}

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

До сих пор это отлично работало для меня, и я использую тот же подход для пула потоков, вещей в System.Environment, Sleep и т. Д. И т. Д.

У Пита Гудлиффа есть серия статей о модульном тестировании многопоточного кода.

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

Для Java, ознакомьтесь с главой 12 JCIP. Есть несколько конкретных примеров написания детерминированных многопоточных модульных тестов, чтобы хотя бы проверить правильность и инварианты параллельного кода.

"Доказывать" безопасность потоков с помощью модульных тестов гораздо сложнее. Я считаю, что это лучше поддается автоматическому интеграционному тестированию на различных платформах / конфигурациях.

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

Я нашел многопоточную библиотеку Java TC из той же группы, в которой была написана FindBugs. Это позволяет вам определять порядок событий без использования Sleep(), и это надежно. Я еще не пробовал это.

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

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

Обновление: я немного поиграл с многопоточной Java-библиотекой TC, и она хорошо работает. Я также перенес некоторые из его функций в версию.NET, которую я называю TickingTest.

Посмотрите на мой соответствующий ответ на

Разработка тестового класса для пользовательского барьера

Он смещен в сторону Java, но имеет разумное резюме вариантов.

В итоге, хотя (IMO) это не использование какой-то причудливой среды, которая будет гарантировать правильность, а то, как вы будете разрабатывать свой многопоточный код. Разделение проблем (параллелизм и функциональность) имеет огромное значение для повышения доверия. Растущее объектно-ориентированное программное обеспечение, управляемое тестами, объясняет некоторые варианты лучше, чем я.

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

Не забывайте, что любые тесты на нагрузку / выдержку редко гарантируют наличие проблем.

Удачи!

Я только недавно обнаружил (для Java) инструмент под названием Threadsafe. Это инструмент статического анализа, очень похожий на findbugs, но специально предназначенный для выявления проблем с многопоточностью. Это не замена для тестирования, но я могу рекомендовать его как часть написания надежной многопоточной Java.

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

Если вы пишете многопоточную Java, попробуйте .

Следующая статья предлагает 2 решения. Обтекание семафора (CountDownLatch) и добавляет функциональность, такую ​​как вывод данных из внутреннего потока. Другим способом достижения этой цели является использование пула потоков (см. Разделы, представляющие интерес).

Sprinkler - расширенный объект синхронизации

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

  1. Event-след / воспроизведения. Для этого требуется монитор событий, а затем просмотр отправленных событий. В рамках UT это будет включать в себя ручную отправку событий как часть теста, а затем выполнение посмертных проверок.
  2. Scriptable. Здесь вы взаимодействуете с работающим кодом с помощью набора триггеров. "На x > foo, baz()". Это может быть интерпретировано в рамках UT, где у вас есть система времени выполнения, запускающая данный тест при определенном условии.
  3. Интерактивный. Это очевидно не будет работать в ситуации автоматического тестирования.;)

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

Мое предложение состояло бы в том, чтобы сосредоточиться на том, чтобы иметь очень строгий проектный протокол о том, что пронизывается, а что нет. Если вы ограничиваете свой интерфейс так, чтобы между элементами были минимальные зависимости, это намного проще.

Удачи, и продолжайте работать над проблемой.

У меня была неудачная задача тестирования многопоточного кода, и это, безусловно, самые сложные тесты, которые я когда-либо писал.

При написании своих тестов я использовал комбинацию делегатов и событий. В основном это все об использовании PropertyNotifyChanged события с WaitCallback или какой-то ConditionalWaiter что опросы.

Я не уверен, что это был лучший подход, но он сработал для меня.

Предполагая, что под "многопоточным" кодом подразумевается нечто, что

  • изменчивый и изменчивый
  • И доступ / изменение несколькими потоками одновременно

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

Поскольку этот зверь встречается редко, прежде всего нам нужно убедиться, что есть все веские основания для его написания.

Шаг 1. Рассмотрим изменение состояния в том же контексте синхронизации.

Сегодня легко написать совместимый и асинхронный код с возможностью компоновки, в котором ввод-вывод или другие медленные операции выгружаются в фоновый режим, но общее состояние обновляется и запрашивается в одном контексте синхронизации. например, задачи async / await и Rx в.NET и т. д. - все они тестируемые по конструкции, "реальные" задачи и планировщики могут быть заменены, чтобы сделать тестирование детерминированным (однако это выходит за рамки вопроса).

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

Шаг 2. Если манипулирование общим состоянием в едином контексте синхронизации абсолютно невозможно.

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

Примечание: если код большой / охватывает несколько классов и требует многопоточных манипуляций с состоянием, тогда очень велика вероятность того, что дизайн не будет хорошим, пересмотрите Шаг 1

Шаг 3. Если этот шаг достигнут, нам нужно протестировать наш собственный настраиваемый потокобезопасный класс / метод / модуль.

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

Если бы мне действительно нужно было протестировать такой код (наконец, реальный ответ), я бы попробовал пару вещей ниже

  1. Недетерминированное стресс-тестирование. Например, запустите 100 потоков одновременно и убедитесь, что конечный результат соответствует. Это более типично для более высокого уровня / интеграционного тестирования многопользовательских сценариев, но также может использоваться на уровне устройства.

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

  3. Тестирование с задержкой для запуска потоков и выполнения операций в определенном порядке. Строго говоря, такие тесты также являются недетерминированными (существует вероятность того, что система GC замораживает / останавливает мир, которая может искажать иным образом организованные задержки), также она уродлива, но позволяет избежать хуков.

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

Механизм, который изначально был изобретен для связи между модулями, представлял собой вызовы функций; когда модуль A хочет связаться с модулем B, он просто вызывает функцию в модуле B. К сожалению, это не работает с потоками, потому что когда вы вызываете функцию, эта функция все еще выполняется в текущем потоке.

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

Условия гонки. Вот что может и будет идти не так.

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

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

Проницательный читатель может заметить, что «Очень подвержен ошибкам» и «Полностью не поддается тестированию» — это смертельная комбинация.

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

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

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

Одним из таких механизмов является передача сообщений, также известная как события.

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

Самое замечательное в передаче сообщений то, что она не страдает от условий гонки и полностью тестируема.

Для кода J2E я использовал SilkPerformer, LoadRunner и JMeter для параллельного тестирования потоков. Все они делают одно и то же. По сути, они предоставляют вам относительно простой интерфейс для администрирования своей версии прокси-сервера, необходимый для анализа потока данных TCP/IP и моделирования одновременного выполнения запросов несколькими пользователями на сервер приложений. Прокси-сервер может дать вам возможность выполнять такие вещи, как анализ выполненных запросов, представляя всю страницу и URL-адрес, отправленный на сервер, а также ответ от сервера после обработки запроса.

Вы можете найти некоторые ошибки в небезопасном режиме http, где вы можете по крайней мере проанализировать отправляемые данные формы и систематически изменять их для каждого пользователя. Но настоящие тесты - это когда вы работаете в https (Secure Socket Layers). Затем вам также придется бороться с систематическим изменением данных сеанса и файлов cookie, что может быть немного более запутанным.

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

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

Предостережение: для понимания этих инструментов требуется некоторое время. Это не будет вопросом простой установки программного обеспечения и запуска графического интерфейса, если вы уже не знакомы с многопоточным программированием. Я попытался определить 3 критические категории областей для понимания (формы, данные сеансов и файлы cookie), надеясь, что, по крайней мере, начиная с понимания этих тем, вы сможете сосредоточиться на быстрых результатах, а не на том, чтобы читать через Вся документация.

По этой теме есть статья, в которой в качестве языка в примере кода используется Rust:

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

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

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

Ссылка на статью полностью написана с использованием модульных тестов.

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

      def process(input):
    # Spawns several threads to do the job
    # ...
    return output

Создайте кучу тестов:

      process(input1) -> expect to return output1
process(input2) -> expect to return output2
...

Теперь запустите каждый из этих тестов много раз.

Если реализациясодержит малозаметную ошибку (например, взаимоблокировку, состояние гонки и т. д.), вероятность появления которой составляет 0,1%, выполнение теста 1000 раз дает вероятность появления ошибки хотя бы один раз в 64%. Запуск теста 10000 раз дает вероятность >99%.

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

Но даже эта библиотека нуждается в хороших знаниях спецификации Java Memory Model, чтобы мы точно знали, что мы тестируем. Но я думаю, что в центре внимания этих усилий находятся миробенчмарки. Не огромные бизнес-приложения.

Это не идеально, но я написал этот помощник для своих тестов на C#:

      using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;

namespace Proto.Promises.Tests.Threading
{
    public class ThreadHelper
    {
        public static readonly int multiThreadCount = Environment.ProcessorCount * 100;
        private static readonly int[] offsets = new int[] { 0, 10, 100, 1000 };

        private readonly Stack<Task> _executingTasks = new Stack<Task>(multiThreadCount);
        private readonly Barrier _barrier = new Barrier(1);
        private int _currentParticipants = 0;
        private readonly TimeSpan _timeout;

        public ThreadHelper() : this(TimeSpan.FromSeconds(10)) { } // 10 second timeout should be enough for most cases.

        public ThreadHelper(TimeSpan timeout)
        {
            _timeout = timeout;
        }

        /// <summary>
        /// Execute the action multiple times in parallel threads.
        /// </summary>
        public void ExecuteMultiActionParallel(Action action)
        {
            for (int i = 0; i < multiThreadCount; ++i)
            {
                AddParallelAction(action);
            }
            ExecutePendingParallelActions();
        }

        /// <summary>
        /// Execute the action once in a separate thread.
        /// </summary>
        public void ExecuteSingleAction(Action action)
        {
            AddParallelAction(action);
            ExecutePendingParallelActions();
        }

        /// <summary>
        /// Add an action to be run in parallel.
        /// </summary>
        public void AddParallelAction(Action action)
        {
            var taskSource = new TaskCompletionSource<bool>();
            lock (_executingTasks)
            {
                ++_currentParticipants;
                _barrier.AddParticipant();
                _executingTasks.Push(taskSource.Task);
            }
            new Thread(() =>
            {
                try
                {
                    _barrier.SignalAndWait(); // Try to make actions run in lock-step to increase likelihood of breaking race conditions.
                    action.Invoke();
                    taskSource.SetResult(true);
                }
                catch (Exception e)
                {
                    taskSource.SetException(e);
                }
            }).Start();
        }

        /// <summary>
        /// Runs the pending actions in parallel, attempting to run them in lock-step.
        /// </summary>
        public void ExecutePendingParallelActions()
        {
            Task[] tasks;
            lock (_executingTasks)
            {
                _barrier.SignalAndWait();
                _barrier.RemoveParticipants(_currentParticipants);
                _currentParticipants = 0;
                tasks = _executingTasks.ToArray();
                _executingTasks.Clear();
            }
            try
            {
                if (!Task.WaitAll(tasks, _timeout))
                {
                    throw new TimeoutException($"Action(s) timed out after {_timeout}, there may be a deadlock.");
                }
            }
            catch (AggregateException e)
            {
                // Only throw one exception instead of aggregate to try to avoid overloading the test error output.
                throw e.Flatten().InnerException;
            }
        }

        /// <summary>
        /// Run each action in parallel multiple times with differing offsets for each run.
        /// <para/>The number of runs is 4^actions.Length, so be careful if you don't want the test to run too long.
        /// </summary>
        /// <param name="expandToProcessorCount">If true, copies each action on additional threads up to the processor count. This can help test more without increasing the time it takes to complete.
        /// <para/>Example: 2 actions with 6 processors, runs each action 3 times in parallel.</param>
        /// <param name="setup">The action to run before each parallel run.</param>
        /// <param name="teardown">The action to run after each parallel run.</param>
        /// <param name="actions">The actions to run in parallel.</param>
        public void ExecuteParallelActionsWithOffsets(bool expandToProcessorCount, Action setup, Action teardown, params Action[] actions)
        {
            setup += () => { };
            teardown += () => { };
            int actionCount = actions.Length;
            int expandCount = expandToProcessorCount ? Math.Max(Environment.ProcessorCount / actionCount, 1) : 1;
            foreach (var combo in GenerateCombinations(offsets, actionCount))
            {
                setup.Invoke();
                for (int k = 0; k < expandCount; ++k)
                {
                    for (int i = 0; i < actionCount; ++i)
                    {
                        int offset = combo[i];
                        Action action = actions[i];
                        AddParallelAction(() =>
                        {
                            for (int j = offset; j > 0; --j) { } // Just spin in a loop for the offset.
                            action.Invoke();
                        });
                    }
                }
                ExecutePendingParallelActions();
                teardown.Invoke();
            }
        }

        // Input: [1, 2, 3], 3
        // Ouput: [
        //          [1, 1, 1],
        //          [2, 1, 1],
        //          [3, 1, 1],
        //          [1, 2, 1],
        //          [2, 2, 1],
        //          [3, 2, 1],
        //          [1, 3, 1],
        //          [2, 3, 1],
        //          [3, 3, 1],
        //          [1, 1, 2],
        //          [2, 1, 2],
        //          [3, 1, 2],
        //          [1, 2, 2],
        //          [2, 2, 2],
        //          [3, 2, 2],
        //          [1, 3, 2],
        //          [2, 3, 2],
        //          [3, 3, 2],
        //          [1, 1, 3],
        //          [2, 1, 3],
        //          [3, 1, 3],
        //          [1, 2, 3],
        //          [2, 2, 3],
        //          [3, 2, 3],
        //          [1, 3, 3],
        //          [2, 3, 3],
        //          [3, 3, 3]
        //        ]
        private static IEnumerable<int[]> GenerateCombinations(int[] options, int count)
        {
            int[] indexTracker = new int[count];
            int[] combo = new int[count];
            for (int i = 0; i < count; ++i)
            {
                combo[i] = options[0];
            }
            // Same algorithm as picking a combination lock.
            int rollovers = 0;
            while (rollovers < count)
            {
                yield return combo; // No need to duplicate the array since we're just reading it.
                for (int i = 0; i < count; ++i)
                {
                    int index = ++indexTracker[i];
                    if (index == options.Length)
                    {
                        indexTracker[i] = 0;
                        combo[i] = options[0];
                        if (i == rollovers)
                        {
                            ++rollovers;
                        }
                    }
                    else
                    {
                        combo[i] = options[index];
                        break;
                    }
                }
            }
        }
    }
}

Пример использования:

      [Test]
public void DeferredMayBeBeResolvedAndPromiseAwaitedConcurrently_void0()
{
    Promise.Deferred deferred = default(Promise.Deferred);
    Promise promise = default(Promise);

    int invokedCount = 0;

    var threadHelper = new ThreadHelper();
    threadHelper.ExecuteParallelActionsWithOffsets(false,
        // Setup
        () =>
        {
            invokedCount = 0;
            deferred = Promise.NewDeferred();
            promise = deferred.Promise;
        },
        // Teardown
        () => Assert.AreEqual(1, invokedCount),
        // Parallel Actions
        () => deferred.Resolve(),
        () => promise.Then(() => { Interlocked.Increment(ref invokedCount); }).Forget()
    );
}

Это может быть полезно. Для этого ограниченного эксперимента достаточно чистых java и junit.

/questions/58492547/kak-pisat-testyi-na-vremya-vyipolneniya-mnogopotochnogo-koda-zakryito/58500137#58500137

Если вы тестируете простой новый Thread(runnable).run(), тогда вы можете смоделировать Thread, чтобы запустить runnable последовательно

Например, если код тестируемого объекта вызывает новый поток, подобный этому

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

Затем может помочь насмешка над новыми потоками и последовательный запуск аргумента runnable

@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 
}

(если возможно) не используйте потоки, используйте актеры / активные объекты. Легко проверить.

Вы можете использовать EasyMock.makeThreadSafe, чтобы сделать тестовый экземпляр безопасным

Другие вопросы по тегам