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; 
    } 
} 
동시에 nullstrings를 볼 것으로 예상된다 getStrings(), 따라서 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() 스레드입니다. 로, loadStrings()로 인해 여러 통화를

@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(); 
} 

이 테스트는 지속적으로 실패

꽤 많은 시간 동안 디버깅 후

좌절 이상한 부분은 내가 JUnit을 4 Mockito 다시 동일한 테스트를 썼다 예상했다.

가 왜 스팍 테스트를 지속적으로 통과 않고, 내가 스팍으로이 테스트에 대해 어떻게 갈 것이라고 문제?

+0

코드의 테스트를 작성하기 위해 코드의 원래 언어 이외의 다른 것을 사용하려는 이유는 무엇입니까? 자신이 알기에 이것은 디버깅을 악몽으로 만듭니다. 언어가 진행될 수 있고 사랑받는 프레임 워크가 뒤에 남아 있다는 사실 외에도. –

+1

@OlegSklyar Spock/Groovy는 간결하고 빠른 방법으로 대부분의 Java 코드를 테스트 할 수있는 훌륭한 도구입니다. Groovy가 Java 바이트 코드로 컴파일하고 Java 디버거를 사용하는 것이 매우 쉽기 때문에 Spock 프레임 워크 자체의 구현과 관련된 문제라고 생각합니다. 또한, 이것은 작업 상황에서 겪었던 문제에 대한 멍청한 예입니다. Spock/Groovy를 도용하거나 Java에서 기본 코드 기반을 마이그레이션 할 수 없습니다. –

+0

불행히도 저는 원래 질문에 대한 대답이 없기 때문에 저에게 그것은 철학적 토론입니다 ... 그러나 더 이상의 프레임 워크를 도입하면 복잡성이 증가합니다. 그 복잡성으로 지금 당신을 물 렸습니다. 나는 당신이 코드를 리팩터링하기로 결정할 때 아름다운 프레임 워크가 모든 테스트를 리펙토링 할 것이라고 믿는다. 분명히 그렇지 않을 것입니다. 자체 제작 했음에도 불구하고 Java 프로젝트 중 하나에서 비슷한 프레임 워크를 처리해야했습니다. Java 테스트 기반 라이브러리를 작성할 때까지 테스트를 수정하거나 수정하는 것은 악몽이었습니다. 이제 모든 테스트는 이해할 수 있고 리팩토링 할 수 있습니다. –

답변

4

문제의 원인은 Spock이 감시하는 메소드가 동기화되는 것입니다. 특히, 이러한 호출이 모두 통과하는 방법 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; 
} 

이렇게하면 Spock이 실수로 동시성 문제를 해결합니다. Mockito는 다른 접근법을 사용합니다.

당신의 검사 결과에 대한 다른 생각을 몇 :

첫째, 당신은 모든 스레드가 따라서 충돌의 가능성을 감소, 같은 순간에 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

당신, 선생님, 전적으로 옳습니다. 이 설명에 대해 대단히 감사합니다! 내 테스트에 관해서는, 나는 그들이 매우 순진하고 우연히 "무작위로"실패 (또는 통과) 할 수 있음을 알고 있습니다. 귀하의 제안에 감사드립니다! –