Какие-нибудь удовлетворительные подходы к модульному тестированию безопасности потока в Java? - PullRequest
40 голосов
/ 08 января 2009

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

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

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

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

Ответы [ 5 ]

20 голосов
/ 08 января 2009

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

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

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

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

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

15 голосов
/ 08 января 2009

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

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

10 голосов
/ 09 февраля 2013

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

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

1 голос
/ 08 января 2009

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

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

0 голосов
/ 20 апреля 2016

Нет ничего плохого в модульном тестировании многопоточного кода, если многопоточность является точкой кода, который вы тестируете (представьте параллельные структуры данных). Общий подход заключается в блокировке основного потока тестирования, выполнении утверждений в отдельных потоках, захвате и повторном отбрасывании любых ошибочных утверждений в основном потоке, в противном случае разблокируйте основной поток тестирования, чтобы можно было завершить тест. Это довольно просто сделать с 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, позволяя тесту пройти или провалиться, как и должно.

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