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

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

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

Ответов (25)

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

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

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

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

Цитировать:

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

...

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

...

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Спринклер - Расширенный объект синхронизации

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

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

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

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

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

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

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

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 
}

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

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

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

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

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

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

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

Это не идеально, но я написал этот помощник для своих тестов на 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()
    );
}

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

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

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

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

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

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

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

Взгляните на мой связанный ответ на

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

Он предвзято относится к Java, но имеет разумное резюме вариантов.

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

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

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

Удачи!

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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