2016-09-17 4 views
5

Субъектом

У меня есть некоторый код, который явно не Потокобезопасный:Тестирование потокобезопасность терпит неудачу с Спком

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(), а ожидалось.

Вопрос

Почему тест Спок последовательно пройти, и как бы я идти о тестировании этого Спока?

+0

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

+1

@OlegSklyar Я нашел Spock/Groovy, чтобы стать отличным инструментом для тестирования большинства Java-кода в краткой и быстрой форме. Я думаю, что проблема здесь заключается в реализации самой структуры Spock, поскольку Groovy компилируется в Java-байт-код, и довольно легко использовать отладчик Java с ним. Кроме того, это необузданный пример проблемы, с которой я столкнулся в рабочей ситуации: я не могу ни остановить Spock/Groovy, ни перенести основную базу кода с Java. –

+0

К сожалению, у меня нет ответа на ваш первоначальный вопрос, поэтому для меня это философская дискуссия ... Но введение каких-либо дополнительных рамок увеличивает сложность. Эта сложность укусила вас сейчас. Я считаю, что прекрасная структура также реорганизует все тесты, когда вы решите реорганизовать код. Конечно, этого не произойдет. Несмотря на самодельное, мне пришлось иметь дело с подобной структурой в одном из проектов Java: кошмару было исправление или изменение тестов, пока мы не написали тестовую базу Java. Теперь все тесты понятны и рефакторизуемы. –

ответ

4

Причиной вашей проблемы является то, что Спок делает методы, которые он шпионит на синхронизированном. В частности, синхронизирован метод MockController.handle(), через который проходят все такие вызовы. Вы легко заметите это, если вы добавите паузу и некоторый вывод в свой метод getStrings().

public List<String> getStrings() throws InterruptedException { 
    System.out.println(Thread.currentThread().getId() + " goes to sleep"); 
    Thread.sleep(1000); 
    System.out.println(Thread.currentThread().getId() + " awoke"); 
    if (strings == null) { 
     strings = loadStrings(); 
    } 
    return strings; 
} 

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

Несколько других мыслей ваших тестов:

Во-первых, вы не слишком много, чтобы гарантировать, что все потоки пришли к getStrings() вызову в то же время, тем самым уменьшая вероятность столкновений. Длительное время может проходить между началом потоков (достаточно долго, чтобы первый выполнил вызов, прежде чем другие начнут его). Лучшим подходом было бы использовать некоторый примитив синхронизации, чтобы удалить влияние времени запуска потоков.Например, CountDownLatch могут быть использованы здесь:

given: 
def final CountDownLatch latch = new CountDownLatch(10) 
def loader = Spy(ExampleLoader) 
def threads = (0..<10).collect { new Thread({ 
    latch.countDown() 
    latch.await() 
    loader.getStrings() 
})} 

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

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

+1

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

 Смежные вопросы

  • Нет связанных вопросов^_^