Development Tip

스레드 코드를 단위 테스트하려면 어떻게해야합니까?

yourdevel 2020. 9. 30. 11:34
반응형

스레드 코드를 단위 테스트하려면 어떻게해야합니까?


나는 그것이 너무 많은 지뢰밭처럼 보이기 때문에 멀티 스레드 코드를 테스트하는 악몽을 지금까지 피했습니다. 성공적인 실행을 위해 스레드에 의존하는 코드를 사람들이 어떻게 테스트했는지, 두 스레드가 주어진 방식으로 상호 작용할 때만 나타나는 문제를 어떻게 테스트했는지 물어보고 싶습니다.

이것은 오늘날 프로그래머들에게 정말 중요한 문제인 것 같습니다. 우리의 지식을이 imho에 대해 모으는 것이 유용 할 것입니다.


이 일을하는 쉬운 방법은 없습니다. 나는 본질적으로 다중 스레드 프로젝트를 진행하고 있습니다. 이벤트는 운영 체제에서 들어오고 동시에 처리해야합니다.

복잡한 다중 스레드 응용 프로그램 코드를 테스트하는 가장 간단한 방법은 다음과 같습니다. 테스트하기에 너무 복잡하면 잘못한 것입니다. 여러 스레드가 작동하는 단일 인스턴스가 있고 이러한 스레드가 서로 겹쳐지는 상황을 테스트 할 수없는 경우 디자인을 다시 수행해야합니다. 이것만큼 간단하고 복잡합니다.

스레드가 동시에 인스턴스를 통해 실행되는 것을 방지하는 다중 스레딩을 프로그래밍하는 방법에는 여러 가지가 있습니다. 가장 간단한 방법은 모든 객체를 변경 불가능하게 만드는 것입니다. 물론 그것은 일반적으로 가능하지 않습니다. 따라서 디자인에서 스레드가 동일한 인스턴스와 상호 작용하는 위치를 식별하고 해당 위치의 수를 줄여야합니다. 이렇게하면 실제로 멀티 스레딩이 발생하는 몇 개의 클래스를 분리하여 시스템 테스트의 전반적인 복잡성을 줄일 수 있습니다.

그러나 이렇게하더라도 두 스레드가 서로 밟는 모든 상황을 테스트 할 수는 없다는 것을 알아야합니다. 이를 위해서는 동일한 테스트에서 두 개의 스레드를 동시에 실행 한 다음 주어진 순간에 실행중인 라인을 정확히 제어해야합니다. 최선의 방법은이 상황을 시뮬레이션하는 것입니다. 그러나이를 위해서는 테스트를 위해 특별히 코드를 작성해야 할 수 있으며, 이는 진정한 솔루션을 향한 기껏해야 절반 단계입니다.

스레딩 문제에 대한 코드를 테스트하는 가장 좋은 방법은 코드의 정적 분석을 사용하는 것입니다. 스레드 코드가 유한 한 스레드 안전 패턴 집합을 따르지 않으면 문제가있을 수 있습니다. VS의 코드 분석에는 스레딩에 대한 지식이 포함되어 있지만 그다지 많지 않을 것입니다.

현재 상황 (그리고 앞으로도 좋은시기가 될 것임)에 따라 멀티 스레드 앱을 테스트하는 가장 좋은 방법은 스레드 코드의 복잡성을 최대한 줄이는 것입니다. 스레드가 상호 작용하는 영역을 최소화하고 가능한 한 최선으로 테스트하고 코드 분석을 사용하여 위험 영역을 식별합니다.


이 질문이 게시 된 지 오래되었지만 여전히 답변이 없습니다 ...

kleolb02 의 대답은 좋은 것입니다. 더 자세히 살펴 보겠습니다.

C # 코드를 연습하는 방법이 있습니다. 단위 테스트의 경우 다중 스레드 코드에서 가장 큰 문제인 재현 가능한 테스트 를 프로그래밍 할 수 있어야 합니다. 그래서 제 대답은 동기식 으로 작동하는 테스트 장치에 비동기 코드를 강제하는 것을 목표로 합니다 .

Gerard Meszardos의 저서 " xUnit Test Patterns " 의 아이디어 이며 "Humble Object"(p. 695)라고합니다. 핵심 로직 코드와 비동기 코드 냄새가 나는 모든 것을 서로 분리해야합니다. 이로 인해 동기식 으로 작동하는 핵심 로직에 대한 클래스가 생성됩니다 .

이를 통해 동기식 으로 핵심 로직 코드를 테스트 수 있습니다. 핵심 로직에서 수행중인 호출의 타이밍을 절대적으로 제어 할 수 있으므로 재현 가능한 테스트를 수행 할 수 있습니다 . 그리고 이것은 코어 로직과 비동기 로직을 ​​분리함으로써 얻을 수있는 이득입니다.

이 핵심 논리는 다른 클래스에 의해 래핑되어야합니다.이 클래스는 핵심 논리에 대한 호출을 비동기 적으로 수신하고 이러한 호출을 핵심 논리에 위임 합니다. 프로덕션 코드는 해당 클래스를 통해서만 핵심 로직에 액세스합니다. 이 클래스는 호출 만 위임해야하므로 많은 논리가없는 매우 "멍청한"클래스입니다. 따라서이 비동기 작업 클래스에 대한 단위 테스트를 최소한으로 유지할 수 있습니다.

그 이상 (클래스 간의 상호 작용 테스트)은 구성 요소 테스트입니다. 또한이 경우 "Humble Object"패턴을 고수하면 타이밍을 절대적으로 제어 할 수 있어야합니다.


참으로 힘든! 내 (C ++) 단위 테스트에서 사용 된 동시성 패턴의 라인에 따라 여러 범주로 분류했습니다.

  1. 단일 스레드에서 작동하고 스레드를 인식하지 않는 클래스에 대한 단위 테스트-평상시처럼 쉽게 테스트하십시오.

  2. 동기화 된 공용 API를 노출하는 Monitor 개체 (호출자의 제어 스레드에서 동기화 된 메서드를 실행하는 개체)에 대한 단위 테스트 -API를 실행하는 여러 모의 스레드를 인스턴스화합니다. 수동적 개체의 내부 조건을 행사하는 시나리오를 구성합니다. 기본적으로 오랜 기간 동안 여러 스레드에서 도대체 능가하는 장기 실행 테스트를 포함하십시오. 이것은 내가 아는 비 과학적이지만 자신감을 키워줍니다.

  3. Active 개체 (자신의 스레드 또는 제어 스레드를 캡슐화하는 개체)에 대한 단위 테스트 -클래스 디자인에 따라 변형이있는 위의 # 2와 유사합니다. 공용 API는 차단 또는 차단되지 않을 수 있으며, 호출자는 선물을받을 수 있으며, 데이터는 대기열에 도착하거나 대기열에서 빼야 할 수 있습니다. 여기에는 많은 조합이 가능합니다. 멀리 흰색 상자. 테스트 대상 개체를 호출하려면 여전히 여러 개의 모의 스레드가 필요합니다.

여담으로:

제가 수행하는 내부 개발자 교육에서 동시성 문제를 생각하고 분해하기위한 기본 프레임 워크로 동시성 기둥 과이 두 패턴을 가르칩니다 . 분명히 더 고급 개념이 있지만이 기본 세트는 엔지니어가 수프에서 벗어나는 데 도움이된다는 것을 발견했습니다. 또한 위에서 설명한대로 더 단위 테스트가 가능한 코드로 이어집니다.


최근 몇 년 동안 여러 프로젝트에 대한 스레드 처리 코드를 작성할 때이 문제에 여러 번 직면했습니다. 대부분의 다른 답변은 대안을 제공하는 동안 테스트에 대한 질문에 실제로 답변하지 않기 때문에 늦은 답변을 제공합니다. 내 대답은 다중 스레드 코드에 대한 대안이없는 경우에 대한 것입니다. 완전성을 위해 코드 디자인 문제를 다루지 만 단위 테스트에 대해서도 논의합니다.

테스트 가능한 다중 스레드 코드 작성

가장 먼저해야 할 일은 실제 데이터 처리를 수행하는 모든 코드에서 프로덕션 스레드 처리 코드를 분리하는 것입니다. 이렇게하면 데이터 처리를 단일 스레드 코드로 테스트 할 수 있으며 다중 스레드 코드가 수행하는 유일한 작업은 스레드를 조정하는 것입니다.

두 번째로 기억해야 할 것은 멀티 스레드 코드의 버그는 확률 적이라는 것입니다. 가장 빈번하게 나타나는 버그는 프로덕션으로 몰래 들어가는 버그이며 프로덕션에서도 재현하기 어렵 기 때문에 가장 큰 문제를 일으킬 것입니다. 이러한 이유로 코드를 빠르게 작성한 다음 작동 할 때까지 디버깅하는 표준 코딩 방식은 멀티 스레드 코드에 좋지 않습니다. 쉬운 버그가 수정되고 위험한 버그가 여전히 존재하는 코드가 생성됩니다.

대신 멀티 스레드 코드를 작성할 때 처음부터 버그를 작성하지 않으려는 태도로 코드를 작성해야합니다. 데이터 처리 코드를 적절하게 제거했다면 스레드 처리 코드는 버그를 작성하지 않고 확실히 많은 버그를 작성하지 않고도 작성할 수있을만큼 충분히 작아야합니다 (가급적이면 몇 줄, 최악의 경우 수십 줄). , 스레딩을 이해한다면 시간을내어 조심하십시오.

다중 스레드 코드에 대한 단위 테스트 작성

멀티 스레드 코드가 최대한 신중하게 작성되면 해당 코드에 대한 테스트를 작성하는 것이 좋습니다. 테스트의 주요 목적은 타이밍 의존성이 높은 경쟁 조건 버그를 테스트하는 것이 아니라 이러한 경쟁 조건을 반복적으로 테스트하는 것이 불가능합니다. 오히려 이러한 버그를 방지하기위한 잠금 전략이 여러 스레드가 의도 한대로 상호 작용할 수 있는지 테스트하는 것입니다. .

올바른 잠금 동작을 올바르게 테스트하려면 테스트에서 여러 스레드를 시작해야합니다. 테스트를 반복 가능하게 만들기 위해 스레드 간의 상호 작용이 예측 가능한 순서로 발생하기를 원합니다. 테스트에서 스레드를 외부 적으로 동기화하는 것은 원하지 않습니다. 이는 스레드가 외부 적으로 동기화되지 않은 프로덕션에서 발생할 수있는 버그를 가리기 때문입니다. 이는 스레드 동기화를위한 타이밍 지연의 사용을 남깁니다. 이것은 멀티 스레드 코드 테스트를 작성해야 할 때마다 성공적으로 사용한 기술입니다.

지연이 너무 짧으면 테스트가 실행될 수있는 다른 시스템 간의 사소한 타이밍 차이로 인해 타이밍이 꺼지고 테스트가 실패 할 수 있으므로 테스트가 취약 해집니다. 내가 일반적으로 수행 한 작업은 테스트 실패를 유발하는 지연으로 시작하여 테스트가 내 개발 컴퓨터에서 안정적으로 통과하도록 지연을 늘린 다음 그 이후의 지연을 두 배로 늘려 다른 컴퓨터에서 테스트를 통과 할 수있는 좋은 기회를 얻었습니다. 이것은 테스트에 거시적 인 시간이 소요된다는 것을 의미하지만, 내 경험상 신중한 테스트 설계는 그 시간을 12 초 이하로 제한 할 수 있습니다. 애플리케이션에 스레드 조정 코드가 필요한 곳이 많지 않아야하므로 테스트 스위트에서 허용 할 수 있습니다.

마지막으로 테스트에서 발견 된 버그의 수를 추적하십시오. 테스트의 코드 범위가 80 %라면 버그의 약 80 %를 잡을 것으로 예상 할 수 있습니다. 테스트가 잘 설계되었지만 버그가 발견되지 않았다면 프로덕션에서만 나타나는 추가 버그가 없을 가능성이 타당합니다. 테스트에서 하나 또는 두 개의 버그가 발견되면 여전히 운이 좋을 수 있습니다. 그 외에도 스레드 처리 코드에 대한 신중한 검토 또는 전체 재 작성을 고려할 수 있습니다. 코드에 코드가 생산 될 때까지 찾기가 매우 어려운 숨겨진 버그가 여전히 포함되어있을 가능성이 높기 때문입니다. 그러면 고치기가 어렵습니다.


또한 다중 스레드 코드를 테스트하는 데 심각한 문제가있었습니다. 그런 다음 Gerard Meszaros의 "xUnit 테스트 패턴"에서 정말 멋진 솔루션을 찾았습니다. 그가 설명하는 패턴을 Humble object 라고 합니다 .

기본적으로 로직을 환경에서 분리 된 별도의 테스트하기 쉬운 구성 요소로 추출하는 방법을 설명합니다. 이 로직을 테스트 한 후 복잡한 동작 (다중 스레딩, 비동기 실행 등)을 테스트 할 수 있습니다.


꽤 좋은 도구가 몇 가지 있습니다. 다음은 일부 Java 기능에 대한 요약입니다.

좋은 정적 분석 도구로는 FindBugs (유용한 힌트 제공), JLint , Java Pathfinder (JPF 및 JPF2) 및 Bogor가 있습니다.

MultithreadedTC 는 자체 테스트 케이스를 설정해야하는 매우 훌륭한 동적 분석 도구 (JUnit에 통합됨)입니다.

IBM Research의 ConTest 는 흥미 롭습니다. 버그를 무작위로 발견하기 위해 모든 종류의 스레드 수정 동작 (예 : 절전 및 양보)을 삽입하여 코드를 계측합니다.

SPIN 은 Java (및 기타) 구성 요소를 모델링하기위한 정말 멋진 도구이지만 몇 가지 유용한 프레임 워크가 필요합니다. 그대로 사용하기는 어렵지만 사용 방법을 알고 있다면 매우 강력합니다. 상당수의 도구가 후드 아래에서 SPIN을 사용합니다.

MultithreadedTC는 아마도 가장 주류이지만 위에 나열된 정적 분석 도구 중 일부는 확실히 살펴볼 가치가 있습니다.


Awaitility 는 결정적 단위 테스트를 작성하는 데 유용 할 수도 있습니다. 시스템의 일부 상태가 업데이트 될 때까지 기다릴 수 있습니다. 예를 들면 :

await().untilCall( to(myService).myMethod(), greaterThan(3) );

또는

await().atMost(5,SECONDS).until(fieldIn(myObject).ofType(int.class), equalTo(1));

또한 Scala 및 Groovy를 지원합니다.

await until { something() > 4 } // Scala example

스레드 코드 및 일반적으로 매우 복잡한 시스템을 테스트하는 또 다른 방법은 Fuzz Testing 입니다. 훌륭하지도 않고 모든 것을 찾을 수는 없지만 유용하고 간단합니다.

인용문:

퍼즈 테스트 또는 퍼징은 프로그램의 입력에 임의의 데이터 ( "퍼즈")를 제공하는 소프트웨어 테스트 기술입니다. 프로그램이 실패하면 (예 : 크래시 또는 내장 코드 어설 션 실패) 결함을 확인할 수 있습니다. 퍼즈 테스트의 가장 큰 장점은 테스트 디자인이 매우 간단하고 시스템 동작에 대한 선입견이 없다는 것입니다.

...

퍼즈 테스트는 블랙 박스 테스트를 사용하는 대규모 소프트웨어 개발 프로젝트에서 자주 사용됩니다. 이러한 프로젝트에는 일반적으로 테스트 도구를 개발할 예산이 있으며 퍼즈 테스트는 비용 대비 높은 이점을 제공하는 기술 중 하나입니다.

...

그러나 퍼즈 테스트는 철저한 테스트 나 공식적인 방법을 대체 할 수 없습니다. 시스템 동작의 무작위 샘플 만 제공 할 수 있으며, 대부분의 경우 퍼지 테스트를 통과하면 소프트웨어가 충돌하지 않고 예외를 처리한다는 것을 보여줄 수 있습니다. 올바르게 작동합니다. 따라서 퍼즈 테스트는 품질 보증이 아닌 버그 찾기 도구로만 간주 될 수 있습니다.


나는 이것을 많이 해왔고, 그렇다.

몇 가지 팁 :

  • 여러 테스트 스레드를 실행하기위한 GroboUtils
  • 계측기 클래스에 대한 alphaWorks ConTest 를 통해 반복간에 인터리빙이 달라집니다.
  • 크리에이트 throwable필드와 그것을 확인 tearDown(목록 1 참조). 다른 스레드에서 잘못된 예외를 포착하면 throwable에 할당하기 만하면됩니다.
  • Listing 2에서 utils 클래스를 만들었고, 특히 waitForVerify와 waitForCondition이 매우 유용하다는 것을 알게되었습니다. 이것은 테스트 성능을 크게 향상시킬 것입니다.
  • AtomicBoolean테스트에서 잘 활용 하십시오. 스레드로부터 안전하며 콜백 클래스 등의 값을 저장하려면 최종 참조 유형이 필요한 경우가 많습니다. 목록 3의 예를 참조하십시오.
  • @Test(timeout=60*1000)동시성 테스트는 때때로 중단 될 때 영원히 중단 될 수 있으므로 항상 테스트에 시간 초과 (예 :)를 제공해야합니다.

목록 1 :

@After
public void tearDown() {
    if ( throwable != null )
        throw throwable;
}

목록 2 :

import static org.junit.Assert.fail;
import java.io.File;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;
import java.util.Random;
import org.apache.commons.collections.Closure;
import org.apache.commons.collections.Predicate;
import org.apache.commons.lang.time.StopWatch;
import org.easymock.EasyMock;
import org.easymock.classextension.internal.ClassExtensionHelper;
import static org.easymock.classextension.EasyMock.*;

import ca.digitalrapids.io.DRFileUtils;

/**
 * Various utilities for testing
 */
public abstract class DRTestUtils
{
    static private Random random = new Random();

/** Calls {@link #waitForCondition(Integer, Integer, Predicate, String)} with
 * default max wait and check period values.
 */
static public void waitForCondition(Predicate predicate, String errorMessage) 
    throws Throwable
{
    waitForCondition(null, null, predicate, errorMessage);
}

/** Blocks until a condition is true, throwing an {@link AssertionError} if
 * it does not become true during a given max time.
 * @param maxWait_ms max time to wait for true condition. Optional; defaults
 * to 30 * 1000 ms (30 seconds).
 * @param checkPeriod_ms period at which to try the condition. Optional; defaults
 * to 100 ms.
 * @param predicate the condition
 * @param errorMessage message use in the {@link AssertionError}
 * @throws Throwable on {@link AssertionError} or any other exception/error
 */
static public void waitForCondition(Integer maxWait_ms, Integer checkPeriod_ms, 
    Predicate predicate, String errorMessage) throws Throwable 
{
    waitForCondition(maxWait_ms, checkPeriod_ms, predicate, new Closure() {
        public void execute(Object errorMessage)
        {
            fail((String)errorMessage);
        }
    }, errorMessage);
}

/** Blocks until a condition is true, running a closure if
 * it does not become true during a given max time.
 * @param maxWait_ms max time to wait for true condition. Optional; defaults
 * to 30 * 1000 ms (30 seconds).
 * @param checkPeriod_ms period at which to try the condition. Optional; defaults
 * to 100 ms.
 * @param predicate the condition
 * @param closure closure to run
 * @param argument argument for closure
 * @throws Throwable on {@link AssertionError} or any other exception/error
 */
static public void waitForCondition(Integer maxWait_ms, Integer checkPeriod_ms, 
    Predicate predicate, Closure closure, Object argument) throws Throwable 
{
    if ( maxWait_ms == null )
        maxWait_ms = 30 * 1000;
    if ( checkPeriod_ms == null )
        checkPeriod_ms = 100;
    StopWatch stopWatch = new StopWatch();
    stopWatch.start();
    while ( !predicate.evaluate(null) ) {
        Thread.sleep(checkPeriod_ms);
        if ( stopWatch.getTime() > maxWait_ms ) {
            closure.execute(argument);
        }
    }
}

/** Calls {@link #waitForVerify(Integer, Object)} with <code>null</code>
 * for {@code maxWait_ms}
 */
static public void waitForVerify(Object easyMockProxy)
    throws Throwable
{
    waitForVerify(null, easyMockProxy);
}

/** Repeatedly calls {@link EasyMock#verify(Object[])} until it succeeds, or a
 * max wait time has elapsed.
 * @param maxWait_ms Max wait time. <code>null</code> defaults to 30s.
 * @param easyMockProxy Proxy to call verify on
 * @throws Throwable
 */
static public void waitForVerify(Integer maxWait_ms, Object easyMockProxy)
    throws Throwable
{
    if ( maxWait_ms == null )
        maxWait_ms = 30 * 1000;
    StopWatch stopWatch = new StopWatch();
    stopWatch.start();
    for(;;) {
        try
        {
            verify(easyMockProxy);
            break;
        }
        catch (AssertionError e)
        {
            if ( stopWatch.getTime() > maxWait_ms )
                throw e;
            Thread.sleep(100);
        }
    }
}

/** Returns a path to a directory in the temp dir with the name of the given
 * class. This is useful for temporary test files.
 * @param aClass test class for which to create dir
 * @return the path
 */
static public String getTestDirPathForTestClass(Object object) 
{

    String filename = object instanceof Class ? 
        ((Class)object).getName() :
        object.getClass().getName();
    return DRFileUtils.getTempDir() + File.separator + 
        filename;
}

static public byte[] createRandomByteArray(int bytesLength)
{
    byte[] sourceBytes = new byte[bytesLength];
    random.nextBytes(sourceBytes);
    return sourceBytes;
}

/** Returns <code>true</code> if the given object is an EasyMock mock object 
 */
static public boolean isEasyMockMock(Object object) {
    try {
        InvocationHandler invocationHandler = Proxy
                .getInvocationHandler(object);
        return invocationHandler.getClass().getName().contains("easymock");
    } catch (IllegalArgumentException e) {
        return false;
    }
}
}

목록 3 :

@Test
public void testSomething() {
    final AtomicBoolean called = new AtomicBoolean(false);
    subject.setCallback(new SomeCallback() {
        public void callback(Object arg) {
            // check arg here
            called.set(true);
        }
    });
    subject.run();
    assertTrue(called.get());
}

MT 코드의 정확성을 테스트하는 것은 이미 언급했듯이 상당히 어려운 문제입니다. 결국 코드에 잘못 동기화 된 데이터 경합이 없는지 확인하는 것으로 귀결됩니다. 이것의 문제는 많은 제어권이없는 스레드 실행 (인터리빙) 가능성이 무한히 많다는 것입니다 ( 기사를 반드시 읽어보십시오 ). 간단한 시나리오에서는 추론을 통해 실제로 정확성을 증명할 수 있지만 일반적으로 그렇지 않습니다. 특히 동기화를 피 / 최소화하고 가장 분명하고 쉬운 동기화 옵션을 선택하지 않으려는 경우.

내가 따르는 접근 방식은 잠재적으로 감지되지 않은 데이터 경쟁이 발생할 가능성을 높이기 위해 동시 테스트 코드를 작성하는 것입니다. 그런 다음 얼마 동안 테스트를 실행했습니다. :) 컴퓨터 과학자가 이런 종류의 도구를 선보인 이야기를 우연히 발견했습니다 (사양에서 테스트를 무작위로 고안 한 다음 동시에 격렬하게 실행하여 정의 된 불변성을 확인합니다). 깨질 것).

그건 그렇고, MT 코드 테스트의 이러한 측면은 여기서 언급되지 않았다고 생각합니다. 무작위로 확인할 수있는 코드의 불변을 식별합니다. 불행히도 이러한 불변성을 찾는 것도 상당히 어려운 문제입니다. 또한 실행 중에 항상 유지되지 않을 수 있으므로 실행 지점이 참일 것으로 기대할 수있는 위치를 찾아서 시행해야합니다. 이러한 상태로 코드 실행을 가져 오는 것도 어려운 문제입니다 (그리고 그 자체로 동시성 문제가 발생할 수 있습니다. 휴, 정말 어렵습니다!

읽을만한 흥미로운 링크 :

  • 결정 론적 인터리빙 : 특정 스레드 인터리빙을 강제 실행 한 다음 불변을 확인할 수있는 프레임 워크
  • jMock Blitzer : 스트레스 테스트 동기화
  • assertConcurrent : 스트레스 테스트 동기화의 JUnit 버전
  • 동시 코드 테스트 : 무차별 대입 (스트레스 테스트) 또는 결정 론적 (불변성)의 두 가지 기본 방법에 대한 간략한 개요

Pete Goodliffe 에는 스레드 코드 단위 테스트 에 대한 시리즈가 있습니다.

어렵습니다. 나는 더 쉬운 방법을 취하고 실제 테스트에서 추상화 된 스레딩 코드를 유지하려고합니다. Pete는 내가하는 방식이 틀렸다고 언급했지만, 분리가 옳았거나 운이 좋았다.


Java의 경우 JCIP의 12 장을 확인하십시오 . 적어도 동시 코드의 정확성과 불변성을 테스트하기 위해 결정 론적 다중 스레드 단위 테스트를 작성하는 몇 가지 구체적인 예가 있습니다.

단위 테스트로 스레드 안전성을 "증명"하는 것은 훨씬 더 간단합니다. 제 생각에는 다양한 플랫폼 / 구성에서 자동화 된 통합 테스트가 더 나은 서비스를 제공한다고 생각합니다.


모든 단위 테스트를 처리하는 것과 같은 방식으로 스레드 구성 요소의 단위 테스트를 처리합니다. 즉, 제어 및 격리 프레임 워크의 반전을 사용합니다. 나는 .Net-arena에서 개발하고 스레딩을 (다른 것들 중에서) 완전히 분리하기가 매우 어렵습니다 (거의 불가능하다고 말하고 싶습니다).

따라서 다음과 같은 (간체) 래퍼를 작성했습니다.

public interface IThread
{
    void Start();
    ...
}

public class ThreadWrapper : IThread
{
    private readonly Thread _thread;

    public ThreadWrapper(ThreadStart threadStart)
    {
        _thread = new Thread(threadStart);
    }

    public Start()
    {
        _thread.Start();
    }
}

public interface IThreadingManager
{
    IThread CreateThread(ThreadStart threadStart);
}

public class ThreadingManager : IThreadingManager
{
    public IThread CreateThread(ThreadStart threadStart)
    {
         return new ThreadWrapper(threadStart)
    }
}

거기에서 IThreadingManager를 구성 요소에 쉽게 삽입하고 선택한 격리 프레임 워크를 사용하여 테스트 중에 스레드가 예상대로 동작하도록 할 수 있습니다.

그것은 지금까지 나를 위해 잘 작동했으며 스레드 풀, System.Environment, Sleep 등의 항목에 대해 동일한 접근 방식을 사용합니다.


내 관련 답변을 살펴보십시오.

사용자 지정 장벽에 대한 테스트 클래스 디자인

Java에 편향되어 있지만 옵션에 대한 합리적인 요약이 있습니다.

요약하자면 (IMO)는 정확성을 보장하는 멋진 프레임 워크를 사용하는 것이 아니라 다중 스레드 코드를 디자인하는 방법입니다. 우려 사항 (동시성 및 기능)을 분리하면 신뢰도를 높이는 데 큰 도움이됩니다. 테스트에 의해 안내되는 성장하는 객체 지향 소프트웨어 는 몇 가지 옵션을 내가 할 수있는 것보다 더 잘 설명합니다.

정적 분석 및 공식 메서드 ( 동시성 : 상태 모델 및 Java 프로그램 참조 )는 옵션이지만 상용 개발에서 제한적으로 사용되는 것으로 나타났습니다.

로드 / 소크 스타일 테스트는 문제를 강조하는 데 거의 보장되지 않는다는 것을 잊지 마십시오.

행운을 빕니다!


저는 병렬 스레드에서 실행할 두 개 이상의 테스트 메서드를 작성하고 각각 테스트 대상 개체를 호출합니다. 저는 Sleep () 호출을 사용하여 다른 스레드의 호출 순서를 조정했지만 실제로는 신뢰할 수 없습니다. 또한 타이밍이 정상적으로 작동 할만큼 충분히 오래 자야하기 때문에 훨씬 느립니다.

FindBugs를 작성한 동일한 그룹에서 Multithreaded TC Java 라이브러리찾았습니다 . Sleep ()을 사용하지 않고 이벤트 순서를 지정할 수 있으며 신뢰할 수 있습니다. 나는 아직 그것을 시도하지 않았습니다.

이 접근 방식의 가장 큰 제한은 문제를 일으킬 것으로 의심되는 시나리오 만 테스트 할 수 있다는 것입니다. 다른 사람들이 말했듯이, 철저하게 테스트하기 위해서는 다중 스레드 코드를 소수의 간단한 클래스로 분리해야합니다.

문제를 일으킬 것으로 예상되는 시나리오를주의 깊게 테스트 한 후에는 한동안 클래스에서 여러 개의 동시 요청을 던지는 비과학적인 테스트가 예상치 못한 문제를 찾는 좋은 방법입니다.

업데이트 : 저는 Multithreaded TC Java 라이브러리를 약간 사용해 보았고 잘 작동합니다. 또한 일부 기능을 TickingTest 라고 부르는 .NET 버전으로 이식 했습니다 .


최근에 (Java의 경우) Threadsafe라는 도구를 발견했습니다. findbugs와 매우 유사하지만 특히 다중 스레딩 문제를 발견하기위한 정적 분석 도구입니다. 테스트를 대체하지는 않지만 신뢰할 수있는 다중 스레드 Java 작성의 일부로 권장 할 수 있습니다.

이중 체크 잠금 패러다임을 사용할 때 클래스 포획, 동시 클래스를 통해 안전하지 않은 객체에 액세스, 누락 된 휘발성 수정자를 발견하는 것과 같은 매우 미묘한 잠재적 인 문제도 포착합니다.

다중 스레드 Java를 작성하는 경우 한 번 시도해보십시오.


다음 기사에서는 두 가지 솔루션을 제안합니다. 세마포어 (CountDownLatch)를 래핑하고 내부 스레드에서 데이터를 외부화하는 것과 같은 기능을 추가합니다. 이 목적을 달성하는 또 다른 방법은 스레드 풀을 사용하는 것입니다 (관심 지점 참조).

스프링클러-고급 동기화 개체


저는 지난주 대부분을 대학 도서관에서 동시 코드 디버깅을 연구하며 보냈습니다. 핵심적인 문제는 동시 코드가 비 결정적이라는 것입니다. 일반적으로 학술적 디버깅은 다음 세 가지 캠프 중 하나에 속합니다.

  1. 이벤트 추적 / 재생. 이를 위해서는 이벤트 모니터가 필요하며 전송 된 이벤트를 검토해야합니다. UT 프레임 워크에서 이것은 테스트의 일부로 이벤트를 수동으로 보낸 다음 사후 검토를 수행하는 것을 포함합니다.
  2. 스크립트 가능. 여기에서 트리거 집합을 사용하여 실행중인 코드와 상호 작용합니다. "On x> foo, baz ()". 이것은 특정 조건에서 주어진 테스트를 트리거하는 런타임 시스템이있는 UT 프레임 워크로 해석 될 수 있습니다.
  3. 인터렉티브. 이것은 분명히 자동 테스트 상황에서는 작동하지 않습니다. ;)

이제 위의 주석가들이 알아 차린 것처럼 동시 시스템을보다 결정적인 상태로 설계 할 수 있습니다. 그러나 제대로 수행하지 않으면 순차 시스템을 다시 설계하는 것입니다.

내 제안은 스레드되는 것과 스레드되지 않는 것에 대해 매우 엄격한 설계 프로토콜을 갖는 데 초점을 맞추는 것입니다. 요소간에 최소한의 종속성이 있도록 인터페이스를 제한하면 훨씬 쉽습니다.

행운을 빕니다. 문제 해결을 계속하십시오.


나는 쓰레드 코드를 테스트하는 불행한 일을 겪었고 그들은 분명히 내가 작성한 테스트 중 가장 어려운 테스트입니다.

테스트를 작성할 때 델리게이트와 이벤트의 조합을 사용했습니다. 기본적으로는 사용에 대한 모든 것입니다 PropertyNotifyChangedA의 이벤트 WaitCallback또는 어떤 종류의 ConditionalWaiter그 여론 조사는.

이것이 최선의 접근 방식인지 확실하지 않지만 저에게 효과적이었습니다.


J2E 코드의 경우 스레드의 동시성 테스트를 위해 SilkPerformer, LoadRunner 및 JMeter를 사용했습니다. 그들은 모두 같은 일을합니다. 기본적으로 TCP / IP 데이터 스트림을 분석하고 여러 사용자가 앱 서버에 동시 요청을하는 것을 시뮬레이션하기 위해 필요한 프록시 서버 버전을 관리하기위한 비교적 간단한 인터페이스를 제공합니다. 프록시 서버는 요청을 처리 한 후 서버의 응답뿐 아니라 서버로 전송 된 전체 페이지와 URL을 표시하여 요청을 분석하는 등의 작업을 수행 할 수있는 기능을 제공 할 수 있습니다.

보안되지 않은 http 모드에서 몇 가지 버그를 찾을 수 있습니다. 여기서 최소한 전송되는 양식 데이터를 분석하고 각 사용자에 대해 체계적으로 변경할 수 있습니다. 그러나 실제 테스트는 https (보안 소켓 레이어)에서 실행할 때입니다. 그런 다음 세션 및 쿠키 데이터를 체계적으로 변경해야하는데, 이는 좀 더 복잡 할 수 있습니다.

동시성을 테스트하는 동안 내가 발견 한 최고의 버그는 개발자가 로그인시 LDAP 서버에 대한 연결 요청을 닫는 데 Java 가비지 수집에 의존했다는 사실을 발견했을 때였습니다. 이로 인해 사용자가 노출되었습니다. 서버가 무릎을 꿇었을 때 무슨 일이 있었는지 분석하려고 할 때 다른 사용자의 세션과 매우 혼란스러운 결과로, 몇 초마다 한 트랜잭션을 거의 완료 할 수 없었습니다.

결국, 당신이나 누군가는 내가 방금 언급 한 것과 같은 실수에 대해 코드를 풀고 분석해야 할 것입니다. 그리고 위에서 설명한 문제를 펼쳤을 때 발생한 것과 같은 부서 간 공개 토론이 가장 유용합니다. 그러나 이러한 도구는 다중 스레드 코드를 테스트하기위한 최상의 솔루션입니다. JMeter는 오픈 소스입니다. SilkPerformer 및 LoadRunner는 독점적입니다. 앱이 스레드로부터 안전한지 정말로 알고 싶다면 대기업이 그렇게합니다. 저는 매우 대기업을 위해 전문적으로이 작업을 수행 했으므로 추측하지 않습니다. 나는 개인적인 경험에서 말하고 있습니다.

주의 사항 : 이러한 도구를 이해하는 데 시간이 걸립니다. 이미 멀티 스레드 프로그래밍에 노출 된 적이 없다면 단순히 소프트웨어를 설치하고 GUI를 실행하는 문제가 아닙니다. 저는 이해해야 할 3 가지 중요한 범주 (양식, 세션 및 쿠키 데이터)를 식별하려고 노력했습니다. 최소한 이러한 주제를 이해하는 것부터 시작하면 빠른 결과에 집중하는 데 도움이 될 것입니다. 전체 문서.


동시성은 메모리 모델, 하드웨어, 캐시 및 코드 간의 복잡한 상호 작용입니다. Java의 경우 적어도 이러한 테스트는 주로 jcstress에 의해 부분적으로 해결되었습니다 . 이 라이브러리의 작성자는 많은 JVM, GC 및 Java 동시성 기능의 작성자로 알려져 있습니다.

그러나이 라이브러리조차도 우리가 테스트하는 것을 정확히 알 수 있도록 Java 메모리 모델 사양에 대한 좋은 지식이 필요합니다. 그러나 저는이 노력의 초점이 mircobenchmarks라고 생각합니다. 거대한 비즈니스 애플리케이션이 아닙니다.


"다중 스레드"코드에서 가정하는 것은

  • 상태 저장 및 변경 가능
  • 동시에 여러 스레드에 의해 액세스 / 수정 됨

다시 말해, 우리는 현재 매우 드문 짐승이 될 사용자 정의 상태 저장 스레드 안전 클래스 / 메서드 / 유닛 테스트에 대해 이야기하고 있습니다.

이 짐승은 드물기 때문에 우선 그것을 쓸 수있는 모든 핑계가 있는지 확인해야합니다.

1 단계. 동일한 동기화 컨텍스트에서 상태 수정을 고려하십시오.

오늘날에는 IO 또는 기타 느린 작업이 백그라운드로 오프로드되지만 공유 상태가 하나의 동기화 컨텍스트에서 업데이트되고 쿼리되는 구성 가능한 동시 및 비동기 코드를 쉽게 작성할 수 있습니다. 예를 들어 async / await 작업 및 .NET의 Rx 등-모두 설계에 의해 테스트 가능하며, "실제"작업 및 스케줄러를 대체하여 테스트를 결정적으로 만들 수 있습니다 (그러나 이것은 문제의 범위를 벗어남).

매우 제한적으로 들릴 수 있지만이 접근 방식은 놀랍게도 잘 작동합니다. 스레드로부터 안전한 상태를 만들 필요없이 전체 앱을이 스타일로 작성할 수 있습니다 (I do).

2 단계. 단일 동기화 컨텍스트에서 공유 상태를 조작하는 것이 절대 불가능한 경우.

바퀴가 재창조되고 있지 않은지 확인하십시오. 작업에 적용 할 수있는 표준 대안이 확실히 없습니다. 코드는 매우 응집력이 있고 하나의 단위 내에 포함되어있을 가능성이 높습니다. 예를 들어 해시 맵이나 컬렉션 등과 같은 표준 스레드로부터 안전한 데이터 구조의 특별한 경우 일 가능성이 높습니다.

참고 : 코드가 크거나 여러 클래스에 걸쳐 있고 다중 스레드 상태 조작이 필요한 경우 디자인이 좋지 않을 가능성이 매우 높습니다. 1 단계를 다시 고려하십시오.

3 단계. 이 단계에 도달하면 자체 사용자 지정 상태 저장 스레드 안전 클래스 / 메서드 / 단위 를 테스트해야합니다 .

솔직히 말해서 그런 코드에 대해 적절한 테스트를 작성할 필요가 없었습니다. 대부분의 경우 1 단계에서, 때로는 2 단계로 이동합니다. 사용자 지정 스레드로부터 안전한 코드를 작성해야했던 마지막 시간은 너무 오래 전에 단위 테스트를 채택하기 전이었습니다. 아마 작성할 필요가 없었을 것입니다. 어쨌든 현재 지식으로.

실제로 그러한 코드를 테스트해야한다면 ( 마지막으로 실제 답변 ) 아래 몇 가지를 시도해 보겠습니다.

  1. 비 결정적 스트레스 테스트. 예를 들어 동시에 100 개의 스레드를 실행하고 최종 결과가 일치하는지 확인합니다. 이는 여러 사용자 시나리오의 상위 수준 / 통합 테스트에 더 일반적이지만 단위 수준에서도 사용할 수 있습니다.

  2. 한 스레드가 다른 스레드보다 먼저 작업을 수행해야하는 결정적 시나리오를 만드는 데 도움이되도록 테스트가 일부 코드를 삽입 할 수있는 테스트 '후크'를 노출합니다. 못 생겼지 만 더 나은 건 생각할 수 없습니다.

  3. 스레드를 실행하고 특정 순서로 작업을 수행하기위한 지연 기반 테스트. 엄밀히 말하면 이러한 테스트는 비 결정적이기도합니다 (시스템 동결 / 세계 GC 수집 중지 가능성이 있으며 그렇지 않으면 조정 된 지연을 왜곡 할 수 있음). 또한 추악하지만 후크를 피할 수 있습니다.


간단한 new Thread (runnable) .run ()을 테스트하는 경우 Thread를 모의하여 runnable을 순차적으로 실행할 수 있습니다.

예를 들어 테스트 된 객체의 코드가 다음과 같은 새 스레드를 호출하면

Class TestedClass {
    public void doAsychOp() {
       new Thread(new myRunnable()).start();
    }
}

그런 다음 새 스레드를 조롱하고 실행 가능한 인수를 순차적으로 실행하면 도움이 될 수 있습니다.

@Mock
private Thread threadMock;

@Test
public void myTest() throws Exception {
    PowerMockito.mockStatic(Thread.class);
    //when new thread is created execute runnable immediately 
    PowerMockito.whenNew(Thread.class).withAnyArguments().then(new Answer<Thread>() {
        @Override
        public Thread answer(InvocationOnMock invocation) throws Throwable {
            // immediately run the runnable
            Runnable runnable = invocation.getArgumentAt(0, Runnable.class);
            if(runnable != null) {
                runnable.run();
            }
            return threadMock;//return a mock so Thread.start() will do nothing         
        }
    }); 
    TestedClass testcls = new TestedClass()
    testcls.doAsychOp(); //will invoke myRunnable.run in current thread
    //.... check expected 
}

(가능하다면) 쓰레드를 사용하지 말고 액터 / 액티브 오브젝트를 사용하세요. 테스트하기 쉽습니다.


EasyMock.makeThreadSafe를 사용하여 테스트 인스턴스를 스레드로부터 안전하게 만들 수 있습니다.

참고 URL : https://stackoverflow.com/questions/12159/how-should-i-unit-test-threaded-code

반응형