Любые удовлетворительные подходы к безопасности потоков модульного тестирования в Java?

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

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

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

Приветствуются любые предложения по инструментам/передовой практике/философии в целом.


person Andrzej Doyle    schedule 08.01.2009    source источник


Ответы (5)


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

Если ваша модель параллелизма усложняется, воспользуйтесь подходящими для этого инструментами, такими как SPIN.

person Rene    schedule 08.01.2009

Java Concurrency in Practice содержит полезную информацию о том, как писать тесты для решения проблем параллелизма. Однако они не являются настоящими модульными тестами. Почти невозможно написать настоящий модульный тест для решения проблемы параллелизма.

В основном это сводится к этому. Создайте кучу тестовых потоков и запустите их. Каждая нить должна

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

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

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

person Craig P. Motlin    schedule 08.01.2009
comment
Это похоже на подход, аналогичный тому, что MultithreadedTC пытается немного абстрагироваться. Как вы сказали, очень помогает, если вы знаете, для чего писать тесты, прежде чем начать! - person Andrzej Doyle; 09.01.2009

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

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

public void testForThreadClash() {
  final CountDownLatch latch = new CountDownLatch(1);
  for (int i=0; i<50; ++i) {
    Runnable runner = new Runnable() {
      public void run() {
        try {
          latch.await();
          testMethod();
        } catch (InterruptedException ie) { }
      }
    }
    new Thread(runner, "TestThread"+i).start();
  }
  // all threads are waiting on the latch.
  latch.countDown(); // release the latch
  // all threads are now running concurrently.
}

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

Второй вопрос сложнее, но есть простое решение. Вопрос в следующем: откуда вы знаете, что ваш молот вызовет столкновение? Конечно, в вашем коде есть синхронизированные блоки для предотвращения конфликтов, поэтому вы не можете ответить на вопрос. Но вы можете. Просто удалите синхронизированные ключевые слова. Поскольку именно синхронизированное ключевое слово делает ваш класс потокобезопасным, вы можете просто удалить их и повторно запустить тест. Если тест действителен, теперь он не пройден.

Когда я впервые написал код выше, он оказался недействительным. Я никогда не видел столкновения. Так что это (пока) недействительный тест. Но теперь мы знаем, как получить четкий ответ на второй вопрос. Мы получаем неправильный ответ, но теперь мы можем изменить тест, чтобы сгенерировать ошибку, которую мы ищем. Вот что я сделал: я просто провел тест 100 раз подряд.

for (int j=0; j<100; ++j) {
  testForThreadClash();
}

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

person MiguelMunoz    schedule 08.02.2013
comment
//Просто удалите синхронизированные ключевые слова//. Изменение исходного кода только для того, чтобы сделать его пригодным для тестирования, должно быть последним вариантом. Как насчет вводимого логического флага, который проверяет, следует ли синхронизироваться или нет? - person KrishPrabakar; 20.01.2016
comment
Удаление синхронизированных ключевых слов — это временное действие, позволяющее убедиться, что ваш тест действителен. Вставка вводимого логического флага, чтобы определить, должны ли вещи быть синхронизированы, кажется гораздо более рискованным. - person MiguelMunoz; 14.12.2016

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

Вот интересная статья (хотя я мало что знаю об этом инструменте): http://today.java.net/pub/a/today/2003/08/06/multithreadedTests.html

person Phil    schedule 08.01.2009

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

@Test
public void shouldDeliverMessage() throws Throwable {
  final Waiter waiter = new Waiter();

  messageBus.registerHandler(message -> {
    // Called on separate thread
    waiter.assertEquals(message, "foo");

    // Unblocks the waiter.await call
    waiter.resume();
  };

  messageBus.send("foo");

  // Wait for resume() to be called
  waiter.await(1000);
}

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

person Jonathan    schedule 20.04.2016