깨지기 쉬운 단위 테스트를 피하는 방법?


24

우리는 3,000 건에 가까운 테스트를 작성했습니다. 데이터는 하드 코딩되었으며 코드 재사용은 거의 없습니다. 이 방법론은 엉덩이에 우리를 물기 시작했다. 시스템이 변경됨에 따라 깨진 테스트를 수정하는 데 더 많은 시간을 소비하게됩니다. 단위, 통합 및 기능 테스트가 있습니다.

내가 찾고있는 것은 관리 가능하고 유지 관리 가능한 테스트를 작성하는 결정적인 방법입니다.

프레임 워크


이것은 Programmers.StackExchange, IMO에 훨씬 더 적합합니다 ...
IAbstract

답변:


21

그것들이 "파손 된 단위 테스트"라고 생각하지 마십시오.

그것들은 당신의 프로그램이 더 이상 지원하지 않는 사양입니다.

이를 "테스트 수정"이라고 생각하지 말고 "새로운 요구 사항 정의"로 생각하십시오.

테스트는 다른 방법이 아닌 응용 프로그램을 먼저 지정해야합니다.

작동한다는 것을 알기 전까지는 작동중인 구현이 있다고 말할 수 없습니다. 테스트 할 때까지 작동한다고 말할 수 없습니다.

당신을 안내 할 몇 가지 참고 사항 :

  1. 테스트 테스트 중인 클래스는 짧고 간단 해야합니다 . 각 테스트는 응집력있는 기능 만 검사해야합니다. 즉, 다른 테스트에서 이미 확인한 사항은 신경 쓰지 않습니다.
  2. 테스트와 개체는 느슨하게 연결되어야합니다. 개체를 변경하면 종속성 그래프가 아래쪽으로 만 변경되고 해당 개체를 사용하는 다른 개체는 영향을받지 않습니다.
  3. 잘못된 것을 작성하고 테스트했을 수 있습니다 . 쉬운 인터페이스 또는 쉬운 구현을 위해 객체가 구축 되었습니까? 후자의 경우 이전 구현 인터페이스를 사용하는 많은 코드를 변경하는 것을 알게 될 것입니다.
  4. 가장 좋은 경우에는 단일 책임 원칙을 엄격히 준수하십시오. 더 나쁜 경우에는 인터페이스 분리 원칙을 준수하십시오. SOLID 원리를 참고하십시오 .

5
+1Don't think of it as "fixing the tests", but as "defining new requirements".
StuperUser

2
시험은 다른 방법은 주위에 먼저 응용 프로그램을 지정해야합니다
treecoder

11

설명하는 것은 실제로 그렇게 나쁜 것은 아니지만 테스트에서 발견 한 더 깊은 문제에 대한 포인터

시스템이 변경됨에 따라 깨진 테스트를 수정하는 데 더 많은 시간을 소비하게됩니다. 단위, 통합 및 기능 테스트가 있습니다.

코드를 변경할 수 있고 테스트가 중단 되지 않으면 의심 스러울 것입니다. 합법적 인 변경과 버그의 차이점은 요청 된 사실이며, 요청 된 것은 테스트에 의해 정의 된 것입니다 (TDD 가정).

데이터는 하드 코딩되었습니다.

테스트에서 하드 코딩 된 데이터는 좋은 것입니다. 테스트는 증거가 아닌 위조로 작동합니다. 계산이 너무 많으면 테스트가 팽팽해질 수 있습니다. 예를 들면 다음과 같습니다.

assert sum([1,2,3]) == 6
assert sum([1,2,3]) == 1 + 2 + 3
assert sum([1,2,3]) == reduce(operator.add, [1,2,3])

추상화가 높을수록 알고리즘에 더 가까워지고 그에 따라 acutal 구현과 자체 비교에 더 가까워집니다.

코드 재사용이 거의 없음

테스트에서 코드를 가장 잘 재사용하는 것은 jUnits 에서처럼 assertThat테스트를 단순하게 유지하기 때문에 imho 'Checks' 입니다. 게다가, 테스트가 코드를 공유하기 위해 리팩토링 될 수 있다면, 테스트 된 실제 코드도 그럴 수 있으므로 리팩토링 된베이스를 테스트하는 테스트로 테스트가 줄어 듭니다.


downvoter가 동의하지 않는 부분을 알고 싶습니다.
keppla

keppla-나는 downvoter가 아니지만 일반적으로 모델의 위치에 따라 단위 수준에서 데이터 테스트보다 개체 상호 작용 테스트를 선호합니다. 데이터 테스트는 통합 수준에서 더 잘 작동합니다.
Ritch Melton

@keppla 전체 항목에 특정 제한된 항목이 포함되어 있으면 주문을 다른 채널로 라우팅하는 클래스가 있습니다. 가짜 주문을 생성하여 4 개의 품목 중 2 개가 제한된 품목으로 채워집니다. 제한된 품목이 추가되는 한이 테스트는 독특합니다. 그러나 허위 주문을 작성하고 두 개의 일반 항목을 추가하는 단계는 다른 테스트에서 사용하는 것과 동일한 설정으로 제한되지 않은 항목 워크 플로우를 테스트합니다. 이 경우 주문에 고객 데이터 설정 및 주소 설정이 필요한 경우 항목과 함께 설정 도우미를 재사용하는 것이 좋습니다. 재사용 만 주장하는 이유는 무엇입니까?
Asif Shiraz

6

나는이 문제도 가지고있다. 내 개선 된 다음과 같은 접근 방법은있다 :

  1. 단위 테스트 가 무언가를 테스트하는 유일한 방법이 아니라면 작성하지 마십시오 .

    단위 테스트의 진단 비용과 수정 시간이 가장 낮습니다. 이를 통해 귀중한 도구가됩니다. 문제는 명백한 마일리지가 다를 수 있기 때문에 단위 테스트는 코드 질량을 유지하는 비용을 감당하기에는 너무 소박하다는 것입니다. 나는 맨 아래에 예를 썼다.

  2. 해당 구성 요소에 대한 단위 테스트와 동등한 경우 어설 션을 사용하십시오 . 어설 션에는 디버그 빌드 전체에서 항상 확인 되는 멋진 속성이 있습니다. 따라서 별도의 테스트 단위로 "직원"클래스 제약 조건을 테스트하는 대신 시스템의 모든 테스트 사례를 통해 직원 클래스를 효과적으로 테스트합니다 . 어설 션은 또한 단위 테스트만큼 코드 질량을 늘리지 않는다는 좋은 특성을 가지고 있습니다.

    누군가 나를 죽이기 전에 : 어설 션에서 프로덕션 빌드가 충돌해서는 안됩니다. 대신 "오류"수준으로 기록해야합니다.

    아직 생각하지 않은 사람에게주의를 기울이려면 사용자 나 네트워크 입력에 대해 아무 것도 주장하지 마십시오. 큰 실수입니다.

    최신 코드 기반에서 어설 션이 분명한 곳이면 어디에서나 단위 테스트를 신중하게 제거했습니다. 이로 인해 전체 유지 보수 비용이 크게 절감되었으며 더 행복한 사람이되었습니다.

  3. 시스템 / 통합 테스트를 선호 하여 모든 기본 흐름과 사용자 경험에 맞게 구현하십시오. 코너 케이스가 여기에있을 필요는 없습니다. 시스템 테스트는 모든 구성 요소를 실행하여 사용자 쪽의 동작을 확인합니다. 따라서 시스템 테스트는 반드시 느리게 진행되므로 중요한 테스트를 작성하십시오 (더 이상, 더 이상은 아님). 가장 중요한 문제를 발견하게됩니다. 시스템 테스트는 유지 관리 오버 헤드가 매우 낮습니다.

    어설 션을 사용하고 있기 때문에 각 시스템 테스트는 동시에 수백 개의 "단위 테스트"를 실행한다는 것을 기억하는 것이 중요합니다. 가장 중요한 것은 여러 번 실행된다는 것이 확실합니다.

  4. 기능적으로 테스트 할 수있는 강력한 API를 작성하십시오. API로 인해 작동하는 구성 요소를 자체적으로 검증하기가 너무 어려운 경우 기능 테스트는 어색하며 의미가 없습니다. 좋은 API 디자인 a) 테스트 단계를 간단하게하고 b) 명확하고 귀중한 주장을 얻습니다.

    기능 테스트는 프로세스 장벽을 가로 질러 일대 다 또는 다 대다 (대다로 다 대다)로 통신하는 구성 요소가있는 경우에 올바르게 달성하기가 가장 어렵습니다. 단일 구성 요소에 더 많은 입력 및 출력이 연결 될수록 기능 테스트는 더 어렵습니다. 실제로 기능을 테스트하려면 이들 중 하나를 분리해야하기 때문입니다.


"단위 테스트를 작성하지 마십시오"문제에 대한 예를 들어 보겠습니다.

TEST(exception_thrown_on_null)
{
    InternalDataStructureType sink;
    ASSERT_THROWS(sink.consumeFrom(NULL), std::logic_error);
    try {
        sink.consumeFrom(NULL);
    } catch (const std::logic_error& e) {
        ASSERT(e.what() == "You must not pass NULL as a parameter!");
    }
}

이 테스트의 저자는 기여하지 않는 일곱 개 라인을 추가했습니다 모두에서 최종 제품의 검증에 있습니다. 사용자는해야 결코 A) 아무도 NULL을 전달 없어야하기 때문에 (그래서이 다음 주장을 쓰기) 또는 b)는 NULL의 경우는 약간 다른 동작이 발생할해야 하나, 이런 일을 볼 수 없습니다. 사례가 (b) 인 경우 실제로 해당 행동을 검증하는 테스트를 작성하십시오.

저의 철학은 구현 아티팩트를 테스트해서는 안된다는 것입니다. 실제 출력으로 간주 될 수있는 것만 테스트해야합니다. 그렇지 않으면 단위 테스트 (특정 구현을 강제하는)와 구현 자체 사이에 기본 대량의 코드를 두 번 쓰는 것을 피할 수있는 방법이 없습니다.

여기서 단위 테스트를위한 좋은 후보자가 있다는 점에 유의해야합니다. 실제로, 단위 테스트가 무언가를 검증하고 이러한 테스트를 작성하고 유지하는 것이 가치가있는 유일한 적절한 수단 인 상황도 있습니다. 내 머리 위에이 목록에는 사소한 알고리즘, API의 노출 된 데이터 컨테이너 및 "복잡한"것으로 보이는 고도로 최적화 된 코드가 포함됩니다 (일명 "다음 사람이 망칠 것").

다음과 같은 구체적인 조언을 드리겠습니다. 유닛 테스트가 중단 될 때 신중하게 삭제하고 "이것이 출력입니까, 아니면 코드를 낭비합니까?"라는 질문을합니다. 당신은 아마도 당신의 시간을 낭비하는 것들의 수를 줄이는 데 성공할 것입니다.


3
시스템 / 통합 테스트 선호-이것은 엄청나게 나쁘다. 시스템은 이러한 (느린!) 테스트를 사용하여 단위 수준에서 빠르게 포착 될 수있는 것을 테스트하는 지점에 도달합니다. 유사하고 느린 테스트가 너무 많아서 실행하는 데 몇 시간이 걸립니다.
Ritch Melton

1
@RitchMelton 토론과는 별도로 새 CI 서버가 필요한 것 같습니다. CI는 그렇게 행동해서는 안됩니다.
Andres Jaan Tack

1
충돌 프로그램 (어설 션이 수행하는 것)은 테스트 러너 (CI)를 죽이지 않아야합니다. 이것이 바로 테스트 러너입니다. 무언가가 그러한 실패를 감지하고보고 할 수 있습니다.
Andres Jaan Tack

1
테스트 어설 션이 아닌 친숙한 디버그 전용 '어설 션'스타일 어설 션은 개발자 상호 작용을 기다리고 있기 때문에 CI를 중단하는 대화 상자를 표시합니다.
Ritch Melton

1
아, 그건 우리의 의견 불일치에 대해 많은 것을 설명 할 것입니다. :) C 스타일 어설 션을 말합니다. 방금 이것이 .NET 질문이라는 것을 알았습니다. cplusplus.com/reference/clibrary/cassert/assert
Andres Jaan Tack

5

단위 테스트가 매력처럼 작동하는 것처럼 보입니다. 그것은 요점의 변화이기 때문에 변경하기에 너무 약한 것이 좋습니다 . 코드 중단 테스트의 작은 변경으로 프로그램 전체에서 오류가 발생할 가능성을 제거 할 수 있습니다.

그러나 분석법이 실패하거나 예기치 않은 결과를 초래할 수있는 조건을 테스트하기 만하면됩니다. 이렇게하면 사소한 것이 아니라 실제 문제가있는 경우 유닛 테스트가 "중단"되기 쉽습니다.

비록 당신이 프로그램을 크게 재 설계하고있는 것 같습니다. 그러한 경우 필요한 모든 작업을 수행하고 이전 테스트를 제거한 후 새 테스트로 교체하십시오. 단위 테스트 복구는 프로그램의 급격한 변화로 인해 수정하지 않은 경우에만 유용합니다. 그렇지 않으면 새로 작성된 프로그램 코드 섹션에 적용하기 위해 테스트를 다시 작성하는 데 너무 많은 시간을 투자하고 있음을 알 수 있습니다.


3

다른 사람들이 더 많은 정보를 얻을 것이라고 확신하지만 내 경험상 다음과 같은 중요한 것들이 있습니다.

  1. 테스트 객체 팩토리를 사용하여 입력 데이터 구조를 빌드하므로 해당 로직을 복제 할 필요가 없습니다. 테스트 설정에 필요한 코드를 줄이기 위해 AutoFixture 와 같은 도우미 라이브러리를 살펴볼 수 있습니다.
  2. 각 테스트 클래스마다 SUT 작성을 중앙 집중화하므로 리팩토링 할 때 쉽게 변경할 수 있습니다.
  3. 테스트 코드는 프로덕션 코드만큼 중요합니다. 자신을 반복하고 있음을 발견하거나 코드가 유지하기 어려운 것처럼 느껴지는 경우 등을 리팩토링해야합니다.

한 테스트를 변경하면 다른 테스트가 중단 될 수 있으므로 테스트에서 코드를 더 많이 재사용 할수록 취약 해집니다. 유지 보수성에 대한 대가로 합리적인 비용 일 수 있습니다. 여기서 그 주장에 들어 가지 않습니다. 그러나 포인트 1과 2가 테스트를 덜 취약하게 만듭니다 (질문이었습니다)는 잘못되었습니다.
pdr

@driis-맞습니다. 테스트 코드에는 실행 코드와 다른 관용구가 있습니다. '공통'코드를 리팩토링하고 IoC 컨테이너와 같은 것을 사용하여 물건을 숨기면 테스트에서 노출되는 디자인 문제를 숨길 수 있습니다.
Ritch Melton

@pdr이 만드는 포인트는 단위 테스트에 유효하지만 통합 / 시스템 테스트에는 "태스크 X에 대한 응용 프로그램 준비"의 관점에서 생각하는 것이 유용 할 것이라고 주장합니다. 적절한 위치로 이동하고 특정 런타임 설정을 설정하고 데이터 파일을 여는 등의 작업이 필요할 수 있습니다. 여러 통합 테스트가 동일한 위치에서 시작되면 여러 테스트에서 코드를 재사용하기 위해 해당 코드를 리팩터링하는 것이 그러한 접근 방식의 위험과 한계를 이해 한다면 나쁘지 않을 수 있습니다 .
CVn

2

소스 코드를 사용하여 테스트를 처리하십시오.

버전 관리, 체크 포인트 릴리스, 이슈 트래킹, "피처 소유권", 계획 및 노력 평가 등-그렇게 되었습니까?-이것이 여러분이 설명하는 것과 같은 문제를 처리하는 가장 효율적인 방법이라고 생각합니다.


1

Gerard Meszaros의 XUnit 테스트 패턴을 확실히 살펴보아야 합니다 . 테스트 코드를 재사용하고 중복을 피하기 위해 많은 레시피 가 포함 된 훌륭한 섹션 이 있습니다.

테스트가 약하면 더블을 테스트하기에 충분히 의지하지 않을 수도 있습니다. 특히, 각 단위 테스트가 시작될 때 객체의 전체 그래프를 재생성하면 테스트의 정렬 섹션이 너무 커져서 정렬 섹션을 상당한 수의 테스트에서 다시 작성해야하는 상황에 처할 수 있습니다. 가장 일반적으로 사용되는 클래스 중 하나가 변경되었습니다. Mocks와 Stub은 관련 테스트 컨텍스트를 갖기 위해 재수 화해야하는 객체의 수를 줄임으로써 여기에서 도움을 줄 수 있습니다.

모의 및 스텁을 통해 테스트 설정에서 중요하지 않은 세부 정보를 가져오고 코드를 재사용하기 위해 테스트 패턴을 적용하면 취약성이 크게 감소합니다.

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.