TDD : Unit Test Automation 제언 RCL

회사에서 목이 쉬도록 Unit Test 하라고 말해도... 대걍 흉내만 내고...

자문을 해 본 결과 크게 두가지 점에서 내가 잘못한 걸 알았다.

1. 단위테스트에 대한 지침이나 Best Practices를 설명하지 않았다. (소스는 SVN에 널려있는데...)
2. 단위테스트 소재가 없다. (대부분이 웹개발자라 직접 DB 접속한다)

우와... 어떻하지? 나참... S/W Architecture, Layer 나누기, Interface 설계하기... 결국 OOP부터 새로 시작해야 하나?
요즘 고민이다.

2번은 각팀의 팀장들이 좀 알아서 해줬으면 하고, 1번에 대해서만 좀 간단하게 말해보자.

기본 내용는 java 쪽 설명서를 기본으로 했고, 내가 좀 각색했다.

1. 단위테스트의 일반적인 지침
  • 되도록 모든 경우에 대해 테스트할 수 있도록 한다. (TestDriven.NET + NUnit + NCover 사용)
  • 성공할 테스트 시나리오 및 실패할 테스트 시나리오 모두 테스트해야 한다.
  • 새롭게 제공되는 API는 테스트를 거친 부분만 무죄, 나머지는 유죄
  • Thread-safe해야할 class는 별도로 증명해야 한다. (특히 Business Application인 경우, 파일등을 다룰 때)
  • Database는 Transactional한지, Deadlock이 걸리는지 테스트해야 한다.
  • Release 전, SVN Commit 전에 꼭 모든 테스트를 거쳐야 한다. (자동으로 with NAnt) (테스트 결과도 같이 배포하는 것이 좋다)
  • 외부 인터페이스와의 테스트는 Mocking을 이용해서 자신의 코드를 테스트한다. (RhinoMocks)
  • 성능테스트는 옳바른 측정기를 이용해야 한다. (RCL.Core.OperationTimer 사용)
2. 테스트 코드 작성자가 작성시 점검해야 할 사항
  • 테스트 대상에 대해 어떻게 테스트 할 것인가?
    테스트 시나리오를 잘 작성하고, 그 시나리오가 알기 쉽게 되어 있다면 좋다. (외국애들은 이걸 위해 Fluent Code라 하고, 영어 표현식으로 Method를 호출 할 수 있는 구조를 만드는게 유행이다.
  • 테스트 대상 코드에 대한 실행의 성공 여부는 어떻게 판단하는가?
    A() 메소드는 실행 후 성공이면 0을 실패면 실패코드(0이 아닌)를 반환한다. 그럼 0인지 아닌지를 판단하면되는구나
    ==> 이것은 결과를 사람이 눈으로 확인해서 테스트의 성공여부를 판단하는 것이 아니라, 테스트 자동화를 위해서이다.
    이렇게 해야 반복적인 테스트가 가능하다.

    예: Assert.AreEqual(0, A(0));  Assert.AreEqual(-1, A(-1));
  • 대상 코드의 예외 조건은 무엇인가?
    일부러 예외를 발생시키려고 했는데, 예외가 발생하지 않았다. 이건 뭐다? 실패다. 예상 결과와 다른 것이 테스트의 실패패이다. 

3. 좋은 테스트란?
 
 좋은 테스트는 'A-TRIP' 해야 한다.
  • 자동적 (Automatic)
    테스트는 혼자 실행되고, 알아서 검증할 수 있도록 코드 작성이 되어야 한다.
    반복적인 빌드 작업 시 자동으로 테스트와 검증이 이루어 져야 한다.

  • 철저함 (Through)
    모든 경우에 대해 테스트가 되도록 해야 한다.

  • 독립적 (Independent)
    하나의 테스트 코드는 하나의 테스트 대상에 집중해야 한다. 단위 테스트란 말이 괜히 생겼나?
    한 테스트 메소드에 여러 테스트 대상이 있다는 것은 어디서 문제가 생겼는지 찾기 힘들다.
    복잡한 class 사용은 최소 단위별로 테스트를 수행하고,
    너무 복잡하다거나 외부 시스템과 연계된 것은 Mocking을 이용해서라도 독립적으로 테스트가 가능해야 한다.

  • 전문적 (Professional)
    좋은 Class 설계를 위한 모든 일반 규칙이 테스트 코드에도 똑같이 적용된다.

4. 무엇을 테스트 할 것인가?

Right-BICEP (오른쪽 이두박근이라는 뜻)
  • Right - 결과가 옳은가?
    예: Assert.AreEqual(expected, target.Method());
  • B - 모든 경계(Boundary) 조건이 correct 한가?
    <경계 조건에서 확인해봐야 할 사항들 - 'CORRECT'>
    형식 일치(Conformance) : 값이 기대한 형식과 같은가? (타입뿐 아니라 포맷 등)
    순서(Ordering) - 적절한 순서로 되어 있거나 그렇지 않은 값인가? 
    예 : 정렬 후 정렬 순서 검증 등
    범위(Range) - 예측한 최대/최소 값 사이에 있는 값인가?
    참조(Reference) - 코드가 자기가 직접제어하지 않는 외부코드를 참조하는가? 그렇다면 그것에 대한 또 다른 테스트 코드가 필요하다.
    존재성(Existence) - 값이 존재하는가? (IsNull, IsEmpty, IsNotNull, IsNotEmpty 등)
    개체 수(Cardinality) - 원하는 정확한 수가 되었는가? (AreEqual, Greater, GreaterThan, Less, LessThan)
    시간 (Time) - 절대/상대 시간 모두 - 모든 것이 순서대로, 제 시간에, 원하는 순서에 일어났는가?

  • I - 역(Inverse) 관계를 확인할 수 있는가?
    역가역성이 있는지 확인하라. 
    y = Sqrt(x) 는 x = y*y 이다. (물론 부동소숫점 때문에 Epsilon을 써야 하지만)

  • C - 다른 수단을 사용해서 결과를 교차 확인(Cross-check) 할 수 있는가?
    테스트 대상의 결과 값을 특정지을 수 없을 때에는, 같은 기능을 할 수 있는 다른 안정된 코드와 비교할 수 있도록 한다.

  • E - 에러조건(Error condition)을 강제로 만들어 낼 수 있는가?
    예상되는 예외를 잘 만들어 낼 수 있는 것도 중요한 테스트이다. NUnit의 ExpectedException을 활용

  • P - 성능 (Performance) 특성이 한계 내에 있는가?
    원하는 성능에 있는지? 테스트 값에 따른 변화가 있는지? Multi-thread 상의 Performance는 어떤지 증명해야 한다.
    NUnit의 MaxTime, TimeOut attribute 활용, 또는 시간 측정값을 예상값과 비교.

다음에는 Code Complete에 있는 내용을 정리해 보겠습니다.(근데 거의 비슷합니다.)