Субъектом
У меня есть некоторый код, который явно не Потокобезопасный:Тестирование потокобезопасность терпит неудачу с Спком
public class ExampleLoader
{
private List<String> strings;
protected List<String> loadStrings()
{
return Arrays.asList("Hello", "World", "Sup");
}
public List<String> getStrings()
{
if (strings == null)
{
strings = loadStrings();
}
return strings;
}
}
Несколько потоков, обращающихся getStrings()
одновременно, как ожидается, чтобы увидеть strings
в null
, и, таким образом, loadStrings()
(которая является дорогостоящей операцией) запускается несколько раз.
Проблема
Я хотел, чтобы сделать код поточно, и как хороший гражданин мира, я написал неисправного Спок спецификации первый:
def "getStrings is thread safe"() {
given:
def loader = Spy(ExampleLoader)
def threads = (0..<10).collect { new Thread({ loader.getStrings() })}
when:
threads.each { it.start() }
threads.each { it.join() }
then:
1 * loader.loadStrings()
}
Приведенный выше код создает и запускает 10 потоков что каждый звонит getStrings()
. Затем он утверждает, что loadStrings()
был вызван только один раз, когда все потоки выполнены.
Я ожидал, что это потерпит неудачу. Однако он последовательно проходит. Какие?
После сеанса отладки с участием System.out.println
и других скучных вещей я обнаружил, что потоки действительно асинхронны: их методы run()
напечатаны в случайном порядке. Тем не менее, первая нить для доступа getStrings()
будет всегда только нить для звонка loadStrings()
.
Странная часть
Разочарованный после некоторого времени проводил отладку, я написал тот же тест с JUnit 4 и Mockito:
@Test
public void getStringsIsThreadSafe() throws Exception
{
// given
ExampleLoader loader = Mockito.spy(ExampleLoader.class);
List<Thread> threads = IntStream.range(0, 10)
.mapToObj(index -> new Thread(loader::getStrings))
.collect(Collectors.toList());
// when
threads.forEach(Thread::start);
threads.forEach(thread -> {
try {
thread.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
});
// then
Mockito.verify(loader, Mockito.times(1))
.loadStrings();
}
Этот тест не подействует из-за многочисленных вызовов loadStrings()
, а ожидалось.
Вопрос
Почему тест Спок последовательно пройти, и как бы я идти о тестировании этого Спока?
Почему вы хотите использовать что-либо, кроме исходного языка кода, для написания теста для кода? Как вы сами узнали, это отлаживает кошмар. Помимо того, что язык может идти вперед, и так любимые рамки остаются. –
@OlegSklyar Я нашел Spock/Groovy, чтобы стать отличным инструментом для тестирования большинства Java-кода в краткой и быстрой форме. Я думаю, что проблема здесь заключается в реализации самой структуры Spock, поскольку Groovy компилируется в Java-байт-код, и довольно легко использовать отладчик Java с ним. Кроме того, это необузданный пример проблемы, с которой я столкнулся в рабочей ситуации: я не могу ни остановить Spock/Groovy, ни перенести основную базу кода с Java. –
К сожалению, у меня нет ответа на ваш первоначальный вопрос, поэтому для меня это философская дискуссия ... Но введение каких-либо дополнительных рамок увеличивает сложность. Эта сложность укусила вас сейчас. Я считаю, что прекрасная структура также реорганизует все тесты, когда вы решите реорганизовать код. Конечно, этого не произойдет. Несмотря на самодельное, мне пришлось иметь дело с подобной структурой в одном из проектов Java: кошмару было исправление или изменение тестов, пока мы не написали тестовую базу Java. Теперь все тесты понятны и рефакторизуемы. –