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

704

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

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

JKP
источник
2
Я думал о том, чтобы опубликовать вопрос по точно такой же проблеме. Хотя Уилл делает много хороших замечаний ниже, я думаю, что мы можем добиться большего. Я согласен, что нет единого «подхода» к чистому решению этого вопроса. Тем не менее, «тестирование как можно лучше» устанавливает очень низкую планку. Я вернусь с моими выводами.
Зак Burlingame
В Java: пакет java.util.concurrent содержит несколько плохо известных классов, которые могут помочь в написании детерминированных тестов JUnit. Посмотрите - CountDownLatch - Семафор - Exchanger
Synox
Можете ли вы дать ссылку на ваш предыдущий вопрос, связанный с модульным тестированием, пожалуйста?
Эндрю Гримм
@
Эндрю
7
Я думаю, что важно отметить, что этому вопросу уже 8 лет, а библиотеки приложений за это время прошли довольно долгий путь. В «современную эпоху» (2016) многопоточная разработка в основном проявляется во встроенных системах. Но если вы работаете с настольным или телефонным приложением, сначала изучите альтернативные варианты. Прикладные среды, такие как .NET, теперь включают инструменты для управления или значительного упрощения, вероятно, 90% распространенных многопоточных сценариев. (asnync / await, PLinq, IObservable, TPL ...). Многопоточный код сложен. Если вы не изобретаете колесо заново, вам не нужно его повторно тестировать.
Пол Уильямс

Ответы:

245

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

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

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

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

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

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

undur_gongor
источник
1
Анализ кода хорош, если вы имеете дело с языком / структурой, которая позволяет это. Например: Findbugs найдет очень простые и легкие проблемы совместного использования со статическими переменными. Чего он не может найти, так это одноэлементных шаблонов проектирования, он предполагает, что все объекты могут быть созданы несколько раз. Этот плагин крайне не подходит для таких фреймворков, как Spring.
Зомби
3
на самом деле есть лекарство: активные объекты. drdobbs.com/parallel/prefer-using-active-objects-instead-of-n/…
Укроп
6
Хотя это хороший совет, я все еще спрашиваю: «Как проверить те минимальные области, где требуется несколько потоков?»
Брайан Рейнер
5
«Если это слишком сложно для тестирования, вы делаете это неправильно» - мы все должны погрузиться в устаревший код, который мы не писали. Как это наблюдение помогает кому-то точно?
Ронна
2
Статический анализ, вероятно, хорошая идея, но это не тестирование. Этот пост действительно не отвечает на вопрос о том, как тестировать.
Уоррен Роса
96

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

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

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

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

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

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

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

Тео Ленндорф
источник
1
Но иногда, если потоки хорошо взаимодействуют друг с другом, тоже нужно что-то тестировать, верно? Определенно я отделю основную логику от асинхронной части после прочтения вашего ответа. Но я все еще собираюсь проверить логику через асинхронные интерфейсы с обратным вызовом работа над всеми потоками.
CopperCash
Как насчет многопроцессорных систем?
Технофил
65

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

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

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

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

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

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

Дэвид Джойнер
источник
51

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

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

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

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

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

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

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

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

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

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

Уоррен Дью
источник
3
Тестирование может выявить только наличие ошибок, а не их отсутствие. Первоначальный вопрос касается проблемы с двумя потоками, в этом случае может быть возможным исчерпывающее тестирование, но часто это не так. Для чего-либо, кроме простейших сценариев, вам, возможно, придется прикусить пулю и использовать формальные методы - но не пропустите юнит-тесты! Написание правильного многопоточного кода сложно, во-первых, но столь же сложная проблема - защитить его от регрессии в будущем.
Пол Уильямс
4
Удивительное резюме одного из наименее понятных способов. Ваш ответ - удар по реальной сегрегации, которую люди обычно игнорируют.
Прас
1
Дюжина секунд - это довольно много, даже если у вас всего несколько сотен тестов этой длины ...
Тоби Спейт
1
@TobySpeight Тесты длинные по сравнению с обычными юнит-тестами. Я обнаружил, что полдюжины тестов более чем достаточно, если многопоточный код правильно спроектирован так, чтобы быть как можно более простым - хотя необходимость в нескольких сотнях многопоточных тестов почти наверняка указала бы на слишком сложную организацию потоков.
Уоррен Дью
2
Это хороший аргумент для того, чтобы сохранить логику вашего потока как можно более отделимой от функциональности (я знаю, гораздо легче сказать, чем сделать). И, если возможно, разбить набор тестов на наборы «каждое изменение» и «предварительная фиксация» (чтобы ваши поминутные тесты не слишком сильно влияли).
Тоби Спейт
22

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

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

ollifant
источник
20

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

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

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

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

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

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

xagyg
источник
16

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
Johan
источник
1
Ожидание блестящее - именно то, что я искал!
Forge_7
14

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

Quote:

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

...

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

...

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

Роберт Гулд
источник
13

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

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

  • 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());
}
Кевин Вонг
источник
2
Тайм-аут - это хорошая идея, но если время теста истекло, любые последующие результаты в этом прогоне являются подозрительными. В тесте по тайм-ауту могут остаться запущенные потоки, которые могут вас испортить.
Дон Киркби
12

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

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

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

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

bennidi
источник
Автор ссылается на рандомизацию в тестировании. Это может быть QuickCheck , который был перенесен на многие языки. Вы можете посмотреть доклад о таком тестировании для параллельной системы здесь
Max
6

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

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

graham.reeds
источник
6
Я прочитал две статьи, опубликованные до сих пор, и я не нашел их очень полезными. Он просто говорит о трудностях, не давая много конкретных советов. Возможно будущие статьи улучшатся.
Дон Киркби
6

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

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

Скотт Бэйл
источник
6

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

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

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

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

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

Дон Киркби
источник
5

Я выполняю модульные тесты резьбовых компонентов так же, как и любые юнит-тесты, то есть с инверсией систем управления и изоляции. Я разрабатываю на .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 и т. Д. И т. Д.

SCIM
источник
5

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

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

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

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

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

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

Удачи!

Тоби
источник
Вы также должны упомянуть tempus-fugitздесь свою библиотеку, которая helps write and test concurrent code;)
Идолон
4

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

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

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

feldoh
источник
3

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

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

Эффи Бар-Шеан
источник
3
Пожалуйста, объясните подходы здесь, внешние ссылки могут быть мертвыми в будущем.
Uooo
2

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

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

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

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

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

Пол Натан
источник
2

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

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

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

Дейл Раган
источник
1

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Кола
источник
0

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

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

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

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

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

Красный петух
источник
0

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

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

Мохан Радхакришнан
источник
0

В этом примере есть статья на тему Rust в качестве языка:

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

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

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

Ссылочная статья полностью написана с использованием юнит-тестов.

gterzian
источник
-1

Если вы тестируете простой новый 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 
}
Авраам Шалев
источник
-3

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

Укроп
источник
2
@OMTheEternity может быть, но это все еще лучший ответ IMO.
Укроп
-5

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

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