Использование TDD для извлечения потокобезопасного кода

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

private TextLineEncoder textLineEncoder;
...
public ProtocolEncoder getEncoder() throws Exception {
    if(textLineEncoder == null)
        textLineEncoder = new TextLineEncoder();
    return textLineEncoder;
}

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

Я спрашиваю об этом на Java, но ответ должен быть более широко применимым.

Ответ 2

Вы можете ввести "поставщик" (действительно простой factory), который отвечает за эту строку:

 textLineEncoder = new TextLineEncoder();

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

Ответ 3

В книге Clean Code есть несколько советов о том, как тестировать параллельный код. Один совет, который помог мне найти ошибки concurrency, запускает одновременно больше тестов, чем процессор имеет ядра.

В мой проект, запуск тестов занимает около 2 секунд на моей четырехъядерной машине. Когда я хочу протестировать параллельные части (для этого есть некоторые тесты), я удерживаю в IntelliJ IDEA горячую клавишу для запуска всех тестов, пока я не увижу в строке состояния, что выполняется 20, 50 или 100 тестовых прогонов. В Windows Task Manager я использую процессор и память, чтобы узнать, когда закончились все тестовые прогоны (использование памяти увеличивается на 1-2 ГБ, когда все они работают, а затем медленно возвращается).

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

Существуют также фреймворки, такие как http://www.alphaworks.ibm.com/tech/contest, которые используют манипуляцию с байт-кодом, чтобы заставить код делать больше переключения потоков, тем самым увеличивая вероятность создания видимых ошибок concurrency.

Ответ 4

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

Ответ 5

Глава 12 Java Concurrency на практике называется "Тестирование параллельных программ". Он документирует тестирование на безопасность и жизнестойкость, но говорит, что это сложный вопрос. Я не уверен, что эта проблема разрешима инструментами этой главы.

Ответ 6

Сверху моей головы вы могли бы сравнить возвращаемые экземпляры, чтобы убедиться, что они действительно являются одним и тем же экземпляром или же они разные? Вероятно, где я начну с С#, я бы предположил, что вы можете сделать то же самое в java