TDD에서 인기있게 사용되는 패턴들에 대한 공부.
TDD 트릭, 디자인 패턴, 리팩토링이 TDD와 어떤 관련이 있는지.

질문

어떻게 테스트할 것인가에 대해 이야기 하기 전에, 기본적인 전략에 대한 질문에 답해야 한다.

  • 테스트 한다는 것은 무엇을 뜻하는가?
  • 테스트를 언제 해야 하는가?
  • 테스트할 로직을 어떻게 고를 것인가?
  • 테스트할 데이터를 어떻게 고를 것인가?

그 어떤 소프트웨어 엔지니어도 아무리 작은 수정도 테스트 없이 릴리즈하지 않는다. 테스트를 하지만, 자동화된 테스트를 갖고있지 않을 뿐이다.
‘이 수정이 다른 부분에 영향을 미치지 않을까?’에 대해, 테스트하는 스트레스를 자동화 테스트에 맡기는 것이다.

테스팅 패턴

격리된 테스트

테스트끼리 서로 아무 영향도 미치지 않아야 한다.

  1. 테스트 결과를 보면, 여러 개의 fail이 맨 처음 하나의 fail로부터 비롯되는 경우가 많은데, 이는 잘못된 테스트이다.
    • 문제가 하나면 fail은 하나여야 한다.
  2. 일부 테스트 실행을 위해, 선행 테스트가 실행되어야 하는 경우 이는 잘못된 테스트이다.
    • 문제가 실행 순서에 상관없이 작동할 수 있어야 한다.
  3. 주어진 문제를 작은 단위로 분리한다.
    • 때론 매우 많은 노력이 필요한 작업이지만, 각 테스트를 실행하기 위한 환경을 쉽고 빠르게 세팅하는 것.
    • 결과적으로 시스템이 응집도는 높고 결합도는 낮은 객체의 모음으로 구성.

전체 애플리케이션 대상의 테스트보다 작은 스케일의 테스트를 하면 독립적이기 쉽다.

테스트 목록

시작하기 전 작성해야 할 테스트 목록을 모두 적어둘 것.
발 디딜 곳이 확실해지기 전에 전진하지 말 것.
테스트 목록을 적어두고, ‘지금’ 할일인지, ‘나중에’ 할일인지 판단하여 결정한다.

테스트 우선

테스트는 대상이 되는 코드를 작성하기 직전에 작성하는 것이 좋다.
테스트를 먼저하면, 테스트하지 않음으로서 발생하는 스트레스가 줄고, 효율적인 작업이 가능하다.

단언 우선

단언(assert)를 테스트 작성시 가장 먼저 쓰고 시작한다.

  • 시스템을 개발할 때 무슨 일부터 하는가?
    • 완료된 시스템이 어떨 거라고 알려주는 이야기부터 작성한다. (user story)
  • 특정 기능을 개발할 때 무슨 일부터 하는가?
    • 기능이 완료되면 통과할 수 있는 테스트부터 작성한다.
  • 테스트를 개발할 때 무슨 일부터 하는가?
    • 완료될 때 통과해야 할 단언부터 작성한다.

단언의 기능

  • 테스트하고자 하는 기능이 어디에 속하는지 정함.
  • 기존의 함수 수정할지, 새로운 메서드를 추가할지, 새로운 클래스를 만들지 정함.
  • 메서드의 이름을 정함.
  • 어떤 식으로 검사할지 정함.
  • 이 테스트가 제안하는 다른 테스트는 무엇이 있을지 고려함.

테스트 데이터

읽을 때 쉽고 따라가기 좋은 데이터를 사용한다.
데이터 값을 산발하는게 아니라, 데이터에 차이가 있다면 그 사이에 의미가 있어야 한다.

  • 1과 2 사이에 개념적 차이가 없다면 1을 사용.
    시스템이 여러 입력을 다운다면, 테스트 역시 여러 입력을 반영해야 한다.
    하지만 세 항목으로 가능한 테스트를 열 개를 하면 안된다.
    동일한 상수의 사용을 피한다.
  • 예를 들어 minus()의 구현에 1-1을 쓰지말고 2-1을 사용. 혹시라도 인자의 순서가 엎어지는 것도 고려하는 테스트를 위함.

명백한 데이터

테스트 자체에 예상되는 값과 실제 값을 포함하고 이 둘 사이의 관계를 드러내기 위해 노력하라.

  • 예를 들면, 환율이 2:1, 수수료가 1.5%일 때 100을 환전하는 테스트를 작성할 때,
    • assertEquals(new TestMoney(49.25, "testmoney"), result)
    • assertEqulas(new TestMoney(100 / 2 * (1 - 0.015), "testmoney"), result)
    • 아래와 같이 작성하면 사용된 숫자와 예상되는 결과 사이의 관계를 알 수 있고,
    • 어떻게 기능을 구현해야 하는지도 다시 확인하게 된다.

한 단계 테스트

다음 테스트를 고를 때의 기준은 새로운 무언가를 가르쳐 줄 수 있으며, 구현할 수 있다는 확신이 드는 테스트이다. ‘아는 것에서 모르는 것으로’ 라는 방향성을 가지고 테스트를 고른다.

시작 테스트

테스트를 맨 처음 시작할 때는, 오퍼레이션이 아무 일도 하지 않을 경우를 먼저 테스트한다.

  • 복잡한 수식 계산에서는, 답이 0이라거나 등 함수를 사용하지 않아도 결과가 나오는 테스트.

설명 테스트

자동화된 테스트가 널리 쓰이기 위해, 테스트를 통해 설명을 요청하고, 테스트를 통해 설명하라.

  • 시퀀스 다이어그램이 있다면, 이를 테스트로 작성해 보는 방법 등.

학습 테스트

외부에서 만든 소프트웨어나 API들을 사용해야할 때, API가 예상대로 작동한다는 확인을 위한 작은 테스트를 만들 수 있다.
통과한다면 API를 제대로 이해한 것이고, 실패한다면 실제 프로그램에 넣었어도 제대로 작동하지 않았을 것이다.

회귀 테스트

시스템에 장애가 생겼을 때 제일 먼저 해야할 일. 장애로 인해 실패하는 테스트, 통과할 경우 장애가 수정되었다고 볼 수 있는 테스트를 간단하게 작성한다.
회귀 테스트는 완벽한 선견지명이 있다면 처음 코딩할 때 작성되어야 할 테스트이다. 어떻게 하면 이 테스르를 애초에 작성할 수 있었을까 생각해본다.
시스템 장애를 쉽게 해결할 수 없다면, 리팩토링을 해야할 수 있고, 이는 설계가 마무리되지 않았다는 것이다.

방향잡기

키보드로 뭘 쳐야할지 안다면

  • 명백한 구현
    잘 모르겠다면
  • 가짜 구현
    올바른 설계가 명확하지 않다면
  • 삼각측량 기법 사용 그래도 잘 모른다면
  • 휴식 그래도…?
  • 코드를 다 지워버리고 다시!

자식 테스트

지나치게 큰 테스트 케이스를 돌리는 방법. 원래 테스트 케이스의 깨지는 부분에 해당하는 작은 테스트 케이스를 작성하고, 그 작은 테스트 케이스가 실행되도록 하라. 그 후에 다시 원래의 큰 테스트 케이스를 추가하라.

  • 큰 테스트를 작성하면, 왜 이렇게 테스트가 클까? 어떤 방식으로 더 작게 만들 수 있었을까?
  • 한 번에 여러 구현이 필요한 테스트의 경우 가짜 구현이나 테스트의 일부분을 잠시 지우기도 함.

모의 객체

비용이 많이 들거나, 복잡한 리소스에 의존하는 객체를 테스트할 때 사용하는 방법이다.

  • 예를 들면 database, 내가 보는 안드로이드 F/W 단에서는 Phone, Tracker 등.
    • DB의 경우, 시작 시간이 오래걸리고, 깨끗하게 유지하기 어렵고, 원격 서버에 위치하는 등의 문제가 있는데, 이를 진짜 DB를 쓰지않고 Mock을 사용함으로써 해결.
      객체의 가시성(visibility)에 대해 격려되어 있다.
  • 가독성이 좋다는 장점이 있음.
  • 실제 객체나 DB의 큰 구조나 데이터를 보지 않아도 되고 이해할 수 있음.
  • 설계 과정에서 커플링이 감소.
    그치만 모의 객체가 진짜 객체와 동일하게 동작하지 않을 수 있으므로 프로젝트에 위험 요소가 하나 추가된다.
  • 모의 객체요 엩스트 집합을 진짜 객체가 사용 가능해질 때 그대로 적용해서 위험을 줄일 수 있음.

셀프 션트

한 객체가 다른 객체와 올바르게 대화하는지 테스트하려면 테스트 대상이 되는 객체가 원래의 대화 상대가 아니라 테스트 케이스와 대화하도록 하면 된다.
테스트 케이스에 별도의 이벤트 리스너를 만들어 사용하면, 테스트 케이스가 일종의 모의 객체처럼 사용된다.
셀프 션트 패턴은 테스트 케이스가 구현할 인터페이스(리스너)를 얻기 위해 인터페이스를 추출해야 한다.

  • 인터페이스 추출 vs 기존 클래스를 블랙박스 테스트 - 둘 중 더 쉬운 방법을 선택.
  • 추출된 인터페이스는 여러 곳에서 쓰이기도 함.
  • 파이썬 같은 언어는 테스트에 필요한 오퍼레이션만 구현하면 되지만, 자바 같은 언어는빈 메서드라도 인터페이스를 구현해야 함.

로그 문자열

메시지 호출 순서가 올바른지 검사하고자 할 때 로그 문자열을 사용한다.

  • 로그 문자열을 가지고 있다가 메시지가 호출될 때마다 그 문자열에 추가하도록 하는 방식.
    특히 옵저버를 구현하고, 이벤트 통보가 원하는 순서대로 발생하는지 확인하고자 할 때 유용하다.
    이벤트 통보만 확인하고, 순서는 상관없다면 문자열 집합을 저장하고 집합 비교를 수행하면 된다.

크래시 테스트

에러 코드(발생하기 힘든 에러 상황)를 어떻게 테스트하는가?
테스트되지 않은 코드는 작동하는 것이 아니다. (박력)
수많은 에러 상황을 어떻게 테스트할 것인가? 작동하길 원하는 부분에 대해서만 하면 된다.

  • file system에 여유 공간이 없을 경우에 대한 테스트를 원한다면, 실제로 큰 파일을 만들 수 있지만, 가짜구현을 만들어서 테스트하는 방식이다.

깨진 테스트

혼자서 프로그래밍을 할 때 프로그래밍을 깨진 상태로 끝마치는 것이 좋다.
마무리되지 않은 문장을 보면 그 전에 무슨 생각을 했었느지 떠올리게 되고, 생각의 실마리를 떠올리고 나면 문장을 마무리하고 계속 진행할 수 있다.
프로그래밍도 똑같이 테스트 케이스를 작성하고 실패를 확인하면 나중에 코딩을 할 때, 어느 작업부터 시작할지 명백히 알 수 있다. 몇 주간의 간극 후에도 개발을 그대로 이어나갈 수 있는 책갈피를 가지게 되는 것이다.

깨끗한 체크인

팀단위 프로그래밍을 할 때 프로그래밍을 모든 테스트가 성공한 상태로 끝마치는 것이 좋다.
내가 마지막으로 코딩한 후 무슨 일이 있을지 알 수 없기 때문에, 확신이 있는 상태로 마무리해야 한다.
테스트가 실패했다면, 작성한 코드를 완전히 이해하지 못했다는 것이고,

  1. 작성한 코드를 날려버리거나
  2. 수정하고 다시 테스트해야 한다.
    테스트 통과를 위해 주석처리를 하는 말도 안되는 행동을 해서는 안된다.

초록 막대 패턴

가짜로 구현하기(진짜로 만들기 전까지만)

실패하는 테스트를 만든 후, 상수를 입력해서 테스트를 통과시킨다.

  1. 테스트가 잘못 구현되어있는 경우 (시간을 들이지 않고) 이를 확인할 수 있음.
  2. 리팩토링 해야하는 부분을 명확하게 찾을 수 있음.
  3. 구체적인 예시를 갖고 시작하면 헷갈리는 일이 적음.

삼각측량

연관성 있는 테스트 두 개를 통해 구현을 증명하는 방법이다.
TDD에서는 예가 두 개 이상일때에만 추상화를 하는 방식으로 보수적인 코딩을 할 수 있다.

  • 즉 추상적인 코드는, 두 개 이상의 예시로 삼각측량을 하고 구현을 하는 방식.
    가짜구현이 감각에 의존한다면 삼각측량은 단순하지만 명확하다.

명백한 구현

어떻게 구현해야할지 확신이 든다면 위와 같은 방식을 쓰지 않고 그냥 구현해버려라.
그리고 테스트가 실패한다면 그제서야 위와 같은 방식을 사용해도 된다.
한 번에 어렵다면, 위와 같은 방식들로 ‘제대로 동작하는 코드’ 후에 ‘깨끗한 코드’ 만들기.

하나에서 여럿으로

컬렉션을 사용한 구현에서는, 단일 항목으로 구현하고 확장하는 방식.


xUnit 패턴

xUnit 계열 테스트 프레임워크를 위한 패턴

단언(assertion)

사람 대신 프로그램이 자동으로 코드가 동작하느닞에 대한 판단을 수행하도록 하라.
테스트를 완전히 자동화하려면 결과 평가에 사람의 판단을 끄집어내고, 컴퓨터가 모든 판단을 해야한다.

  • 판단 결과가 boolean 이어야 함.
  • 이 boolean이 컴퓨터에 의해 검증되어야 함.
  • assertEquals(), assertTrue(), assertFalse()

픽스처(fixture)

여러 테스트에서 공통으로 사용되는 객체들을 생성할 때, 각 테스트 코드에 있는 지역 변수를 인스턴스 변수로 바꾸고 setUp()을 정의하여 초기화하도록 한다.

  • 복붙을 한다고 해도 반복 작성에 시간이 소요되는데, 이것을 단축할 수 있다.
  • setup에 수정이 필요한 경우, 반복 수정을 줄일 수 있다.
  • (-) 테스트 작성 전에 setUp()이 수행된다는 점과 어떻게 초기화되는지를 확인해야 한다.
  • 너무 많은 setUp 코드는 작성 전 많은 것들을 기억하게 하므로 호불호가 갈린다.

외부 픽스처

픽스처 중 외부 자원이 있을 경우 tearDown()을 통해 자원을 해제한다.
tearDown()testMethod() 에서 무슨일이 생기든 호출된다. 하지만, setUp() 과정 중 문제가 발생하면 호출되지 않는다.
테스트의 목적 중 하나는 테스트 실행 전과 실행 후의 외부 세계가 동일하게 유지되야 하는 것이다.

  • 따라서 테스트 중 파일을 열었다면 꼭 테스트 끝나기전에 닫아주어야 함.

테스트 메서드

객체지향 프로그래밍 언어에서는 아래와 같이 구조 계층을 나눈다.

  1. 모듈
  2. 클래스
    • 픽스처를 사용하고 이를 위해 클래스를 사용한다면, 같은 픽스처를 공유하는 메서드들이 동일한 클래스에서 작성될 것이다.
  3. 메서드
    • test-로 시작하는 메서드 이름을 xUnit에서는 자동으로 테스트로 인식하고 testsuite를 생성한다.
    • 딱봐도 이해하기 쉬운 네이밍과, 코드. 최대한 간단하고 짧은 코드.

예외 테스트

예외가 발생하는 것이 정상인 경우에 대한 테스트. 예상되는 예외를 잡아서 무시하고, 예외가 발생하지 않는 경우에 테스트가 실패하게 만들면 된다.

  • 정확하게 원하는 종류의 예외만 잡아야 하는 것에 주의.
    public void testWantException() {
    try {
      // do method which is correct making exception.
      fail(); // fail if it is not catched
    } catch (WhatYouReallyWantException e) {}
    }
    

    fail()은 xUnit에서 테스트 실패를 알려주기 위한 메서드이다.

전체 테스트

모든 테스트를 한번에 실행하기 위해서는 모든 테스트 슈트에 대한 모음을 작성하면 된다.

  • 각각의 패키지에 대해 하나씩, 그리고 전체 애플리케이션에서 패키지 테스트를 모아주는 테스트 슈트.