Contents
테스트 주도 개발 7주차
   Nov 25, 2022     9 min read

스터디 7주차(22.11.17 ~ 22.11.26)

진행 범위 : 26장 ~ 27장


26장. 빨간 막대 패턴

  • 이번26장은 테스트를 언제 어디서 작성할 것인지, 테스트 작성을 언제 멈출지에 대한 것

➡️한 단계 테스트

  • 목록에서 테스트를 고를 때 무엇을 기준으로 선택해야할까?
    • 구현할 수 있다는 확신이 드는 테스트를 고를 것.
  • 예를 들어 아래와 같은 목록이 있다면 어떤 테스트를 우선순위로 할 것인가?
- 더하기

- 빼기

- 곱하기

- 나누기

- 비슷한 것 더하기

- 동치성(equals)

- Null과의 동치성(equals null)

- 널 환전

- 한 개의 통화를 환전하기

- 두 개의 통화를 환전하기

- 환시세
  • 전체 계산 중 간단한 하나의 사례를 나타내는 테스트에서 시작했다면, 이 테스트를 통해 자라는 프로그램은 하향식(top-down)으로 작성된 것으로 보일 수 있다.

  • 전체의 작은 한 조각을 나타내는 테스트에서 시작하여 조금씩 붙여나가는 식이었다면, 이 프로그램은 상향식(bottom-up)으로 작성된 것으로 보일 수 있다.

상향식, 하향식 방법은 TDD의 프로세스를 효과적으로 설명해 줄 수 없다.

  1. 수직적 메타포는 프로그램이 시간에 따라 어떻게 변해 가는지에 대한 단순화된 시각일 뿐…
  2. 메타포가 어떤 방향성을 가질 필요가 있다면(상향 혹은 하향보다는) ‘아는 것에서 모르는 것으로(known-to-unknown)’라는 방향이 유용하다.

➡️시작 테스트

어떤 테스트부터 시작하는 게 좋을까?

  • 첫 걸음으로 현실적인 테스트를 하나 작성한다면 많은 문제를 한번에 해결해야 하는 상황이 될 것이기 때문에 유의해야한다.
    1. 이 오퍼레이션을 어디에 두어야 하나?
    2. 적절한 입력 값은 무엇인가?
    3. 이 입력들이 주어졌을 때 적절한 출력은 무엇인가?
  • 상황 예시
    • XP 튜스 그룹에서 누군가가 다각형 축소기(polygon reducer)를 테스트 우선으로 어떻게 작설할지 질문을 했다고 가정해보자. 입력은 다각형 그물이고, 출력은 정확하게 똑같은 표면이면서 가능한 한 최소 개수의 다각형으로 구성된 다각형 그물이 된다.
  • 위의 상황 가정에서 “테스트를 작동하도록 하는 데 박사 학위 논문을 읽어야 하는 경우, 이 문제를 어떻게 테스트 주도로 접근할 수 있을까?”

시작 테스트 패턴에 이 문제에 대한 답을 준다.

  • 출력이 입력과 같은 경우가 있다. 어떤 형상(configuration)의 다각형들은 이미 정규화되어 있고 더 축소할 수 없다.
  • 입력은 가능한 한 적어야 한다. 이를테면 다각형 하나 또는 아예 비어있는 다각형 목록일 수도 있다.
Reducer r = new Reducer(new Polygon());
assertEquals(0, reducer.result().npoints);

첫 번째 테스트 돌아간다. 이제 목록에 있는 나머지 테스트를 처리할 차례다.

한 단계 테스트는 시작 테스트에도 적용된다. 뭔가를 가르쳐줄 수 있으면서도 빠르게 구현할 수 있는 테스트를 선택하자.

만약 어떤 애플리케이션을 n번째 구현하고 있다면, 오퍼레이션을 한두 개 필요로 하는 테스트를 하나 고르는게 좋다.

보통 시작 테스트는 그 이후의 테스트에 비해 좀 더 높은 차원의 테스트로, 애플리케이션 테스트와 비슷하다.

글쓴이가 많이 사용하는 테스트 주도로 개발하는 예 중 하나는 간단한 소켓 기반 서버다.

StartServer
Socket = new Socket
Message = "hello";
Socket.write(message)
AssertEquals(message, socket.read)

➡️설명 테스트

자동화된 테스트가 더 널리 쓰이게 하려면 어떻게 해야 할까?

  • 결론부터 말하면 테스트를 통해 설명을 요청하고 테스트를 통해 설명

단순한 시작법은 테스트를 이용하여 묻고, 테스트를 이용하여 설명하는 것이다.


➡️ 학습 테스트

  • 외부에서 만든 소프트웨어에 대한 테스트를 작성해야할 때도 있을까?

패키지의 새로운 기능을 처음으로 사용해보기 전에 작성한다.


➡️ 회귀 테스트

  • 시스템 장애가 보고될 때 장애로 인한 실패하는 테스트, 장애가 수정되었다고 확인할 수 있는 테스트를 작성한다.

  • 회귀 테스트를 작성할 때는 이 테스트를 작성해야 한다는 사실을 어떻게 하면 애초에 알 수 있었을지 항상 생각해보자.

  • 전체 애플리케이션 차원에서 테스트를 수행하는 것에서도 가치를 얻을 수 있다.

  • 회귀 테스트는 작은 차원에서 테스트를 개선하는 방법이 된다.
    • 예) 큰 음수에 대한 결함보고서
  • 시스템 장애를 손쉽게 격리시킬 수 없다면 리팩토링 해야한다. → 설계 미흡

27장. 테스팅 패턴

  • 이번 장은 테스트 작성법에 대하여 더 상세한 테스트

➡️자식 테스트

규모가 큰 테스트 케이스를 어떻게 돌아가도록 할 수 있을까?

  • 테스트 케이스의 깨지는 부분에 해당하는 작은 테스트 케이스를 작성하고 그 작은 테스트 케이스가 실행되도록 하라. 그 후 다시 큰 테스트 케이스를 추가하라.
  • 신경쓰이는 테스트는 삭제하고 테스트한다. 혹은 삭제말고 제외만하고 테스트를 진행한다.

테스트가 너무 크면 자식처럼 부담을 나누라는 뜻


➡️모의 객체

비용이 많이 들거나 복잡한 리소스에 의존하는 객체를 테스트하려면 어떻게 해야 할까?

  • 상수를 반환하게끔 만든 속임수 버전의 리소스를 만들면 된다. → 모의 객체(Mock Object)

  • 예) 데이터 베이스
    • 데이터베이스는 시작 시간이 오래 걸리고, 깨끗한 상태로 유지하기가 어렵다.
    • 만약 데이터베이스가 원격 서버에 있다면 이로 인해 테스트 성공 여부가 네트워크 상의 물리적 위치에 영향을 받게 된다.
    • 데이터베이스는 개발 중 많은 오류의 원인이 된다.
  • 해법)
    • 데이터베이스를 사용하지 않는 것이다.
    • 대다수의 테스트는, 마치 데이터베이스인 것처럼 행동하지만 실제로는 메모리에만 존재하는 객체를 통해 작성될 수 있다.
public void testOredrLookup(){
    Database db = new MockDatabase();
    db.expectQuery("select order_no from Order where cust_no is 123");
    db.returnResult(new String[] {"Order 2", "Order 3"});
    ...
}

MockDatabase 는 예상된 쿼리를 얻지 못하면 예외를 던질 것이다. 만약 쿼리가 올바르다면 MockDatabase는 상수 문자열에서 마치 결과 집합(result set) 처럼 보이는 뭔가를 생성하여 반환하다.

  • 장점
    • 성능 & 견고함 - 실제 물리적 테스트 환경에 영향을 받지 않고 테스트를 수행할 수 있게 도와준다.
    • 가독성 - 실제 결과를 내는 객체를 사용하면, 왜 그 결과를 내야 하는지 알기 어려운 경우가 있을 수 있다.
    • 모의 객체는 모든 객체의 가시성(visibility)에 대해 고민하도록 하게해서 설계에서 커플링은 감소하도록 한다.
  • 만약 모의 객체가 진짜 객체와 동일하게 동작하지 않으면 어떻게 될까? → 모의 객체용 테스트 집합을 진짜 객체가 사용 가능해질 때 그래도 적용해서 이러한 위험을 줄일 수 있다.

➡️셀프 션트

한 객체가 다른 객체와 올바르게 통신하고 있는지 테스트하려면 어떻게 할까?

  • 테스트 대상이 되는 객체가 원래의 대화 상대가 아니라 테스트 케이스와 대화하도록 만들면 된다.

  • 예시 - 초록 막대를 동적으로 업데이트 하고자 하는 상황

    • UI객체를 TestResult와 연결할 수 있다면 테스트가 실행된 시점, 테스트가 실패한 시점, 전체 테스트 슈트가 시작되고 끝난 시점 등을 통보 받을 수 있을 것이다.
    • 이러한 이벤트를 통보받으면 인터페이스를 갱신하면 된다.
# ResultListenerTest
def testNotification(self):
    result = TestResult()
    listener = ResultListener()
    result.addListener(listener)
    WasRun("testMethod").run(result)
    assert 1 == listener.count

위 테스트가 수행되려면 이벤트 통보 횟수를 셀 객체가 필요하다.

# ResultListener
class ResultListener:
    def __init__(self):
        self.count = 0
    def startTest(self):
        self.count = self.count + 1

왜 이벤트 리스너를 위해 별도의 객체를 만들어야 하는 걸까?

  • 그냥 테스트 케이스 자체를 리스너로 쓰면 될 텐데 말이다. 즉, 테스트 케이스가 일종의 모의 객체의 역할을 하는 것이다.
# ResultListenerTest
def testNotification(self):
    self.count = 0
    result = TestResult()
    result.addListener(self)
    WasRun("testMethod").run(result)
    assert 1 == self.count
def startTest(self):
    self.count = self.count + 1
  • 셀프 션트 패턴은 테스트 케이스가 구현할 인터페이스를 얻기 위해 인터페이스 추출(Extract Interface) 을 해야 한다.
  • 인터페이스를 추출하는 것이 더 쉬운지, 존재하는 클래스를 블랙 박스로 테스트하는 것이 더 쉬운지는 당신이 결정해야할 것이다.

  • 자바의 경우
    • 셀프 션트를 사용한 결과로 인터페이스 안의 온갖 기괴한 메소드들을 다 구현한 테스트들을 보게 될 것이다.

    • 낙관적 타입 시스템(동적인 타입 검사를 수행하는 언어들)을 가진 언어에서는 테스트 케이스 클래스가 실제로 테스트를 수행하는 데 꼭 필요한 오퍼레이션들만 구현하면 된다.
    • 하지만 자바에서는 빈 메서드라도 인터페이스의 모든 오퍼레이션들을 구현해야 한다. 그러므로 가능한 한 인터페이스를 작게 만들길 원할 것이다.
    • 인터페이스에 대한 구현은 또한 적절한 값을 되돌리거나 부적절한 오퍼레이션이 호출된 경우 예외를 던지지게끔 만들어야 할 것이다.
  • 낙관적 타입 시스템 : 동적인 타입 검사를 수행하는 언어들 (파이썬 등등..)
  • 비관적 타입 시스템 : 정적인 타입 검사를 수행하는 언어들 (자바 등등.. )

➡️로그 문자열

메시지의 호출 순서가 올바른지를 검사하려면 어떻게 해야 할까?

  • 로그 문자열을 가지고 있다가 메시지가 호출될 때 마다 그 문자열 에 추가하도록 한다.

  • 예시)

    • setUp() → 테스트를 수행하는 메소드
    • tearDown() → 순서로 호출되길 원하는 템플릿 메소드(Template Method)
    • 각 메소드들이 로그 문자열에 자기 이름을 추가하게 구현하면 쉽게 읽히는 테스트를 만들 수 있다.
def testTemplateMethod(self):
    test = WasRun("testMethod")
    result = TestResult()
    test.run(result)
    assert("setUp testMethod tearDown " == test.log)

# WasRun
def setUp(self):
    self.log = "setUp "
def testMethod(self):
    self.log = self.log + "testMethod "
def tearDown(self):
    self.log = self.log + "tearDown "

로그 문자열은 옵저버(Observer)를 구현하고, 이벤트 통보가 원하는 순서대로 발생하는지를 확인하고자 할 때 유용하다.

어떤 이벤트 통보들이 일어나는지를 검사하기는 하지만 그 순서는 상관이 없을 경우엔 문자열 집합을 저장하고 있다가 단언(assertion)에서 집합 비교를 수행하면 된다.


➡️크래시 테스트 더미

호출되지 않을 것 같은 에러 코드(발생하기 힘든 에러 상황)를 어떻게 테스트할 것인가 ?

  • 작업을 수행하는 대신 그냥 예외를 발생시키기만 하는 특수한 객체를 만들어서 이를 호출한다.

  • 예시) 파일을 위한 크래스 테스트 더미(Crash Test Dummy)

private class FullFile extends File{
    public FullFile(String path) {
        super(path);
    }

    public boolean createNewFile() throws IOException{
        throw new IOException();
    }
}

예상되는 예외를 테스트할 수 있다.

public void testFileSystemError() {
    File f = new FullFile("foo");
    try{
        saveAs(f);
        fail();
    }catch (IOException e) {

    }
}
  • 객체 전체를 흉내낼 필요가 없다는 점을 제외하면 모의 객체와 유사하다.
  • 자바의 익명 내부 클래스(annoymous inner class)는 우리가 테스트하기 원하는 적절한 메서드만이 오류를 발생시키게끔 하기 위해 유용하게 쓰인다.
  • 테스트 케이스 안에서 원하는 메소드 하나만 재정의할 수 있다.
public void testFileSystemError() {
    File f = new File("foo"){
        public boolean createNewFile() throws IOException {
            throw new IOException();
        }
     };

     try {
         saveAs(f);
         fail();
     }catch (IOException e ){
     }

}

이렇게 하면 테스트 읽기가 수월해진다.


➡️깨진 테스트

혼자서 프로그래밍 할 때 프로그래밍 세션을 어떤 상태로 끝마치는 게 좋을까?

  • 마지막 테스트가 깨진 상태로 끝마치는 것이 좋다.

➡️깨끗한 체크인

팀 프로그래밍 할 때 프로그래밍 세션을 어떤 상태로 끝마치는 게 좋을까?

  • 모든 테스트가 성공한 상태로 끝마치는 것이 좋다.

  • 팀 프로젝트에서 프로그래밍 세션을 시작하는 경우라면 자신이 마지막으로 코딩한 다음부터 무슨 일이 있었는지 세밀하게 알 수 없다. 안심되고 확신이 있는 상태에서 시작할 필요가 있다.

  • 체크인하기 전에 실행하는 테스트 스위트(test suite)는 작업 중에 분 단위로 실행하는 테스트 슈트보다 더 클 것이다.

  • 가장 단순한 규칙은 그 동안 작업한 코드를 날려버리고 다시 하는 것이다.


7주차 회고

3부는 이론적인 부분이 강하다.

스터디하는 동안 이해가 잘 안되서 고생이 많았다.

특히 디자인 패턴에 대한 기본 지식이 없다보니 어렵다고 심리적으로 생각하는 것 같다.

혼자 취준생이고 다들 현직자라서 물어보면 잘 알려주신다.

따라가기 좀 힘들지만 매주 정리는 꼭 하자.