[TDD] 27장 ~ 30장 정리

작성일 : 2023년 07월 11일
  • #TDD

테스트 주도 개발(TDD) 책을 보며 공부했던 내용을 간단히 요약 정리 해본다.

27장. 테스팅 패턴

자식 테스트


지나치게 큰 테스트를 어떻게 돌아가도록 할 수 있을까?

원래 테스트 케이스의 깨지는 부분에 해당하는 작은 테스트 케이스를 작성하고 그 작은 테스트 케이스가 실행되도록 하라.

그 후에 다시 원래의 큰 테스트 케이스를 추가하라.


순차적인 단계를 유지 하도록 하자 (빨강 -> 초록 -> 리팩토링)


자신만의 방법을 찾을 것


모의 객체


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

상수를 반환하게끔 속임수 버전의 리소스를 만들면 된다.

데이터베이스는 외부 요인에 의한 변수가 많다.


해법은 대부분의 경우 진짜 데이터 베이스를 사용하지 않는것.


성능과 견고함에 더해 모의 객체는 가독성이 더 좋다.


모의 객체가 진짜 객체와 동일하게 동작하지 않으면 어떻게 될까?

  • 모의 객체용 테스트 집합을 진짜 객체가 사용 가능해질때 그대로 적용해서 위험을 줄임.

셀프 션트


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

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

테스팅 사용자 인터페이스의 초록 막대를 동적으로 업데이트 상황


UI 객체를 TestResult와 연결할 수 있다면 테스트가 실행된 시점, 테스트가 실패한 시점,


전체 테스트슈트가 시작되고 끝난 시점 등을 통보 받을수 있을 것.

  • 이러한 이벤트를 통보 받으면 인터페이스를 갱신하면 됨.

셀트 션트 패턴을 이용해 작성한 테스트가 그렇지 않은 테스트보다 읽기에 더 수월.


셀프 션트 패턴은 테스트 케이스가 구현할 인터페이스를 얻기 위해 인터페이스 추출 해야함.


셀프 션트를 위해 추출해 낸 인터페이스는 여러 곳에서 쓰이는 경우가 많음.


로그 문자열


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

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

메시지의 호출 : 메서드의 호출


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


로그 문자열을 셀프션트와도 잘 작동함.


크래시 테스트 더미


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

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

가짜 구현(fake it)을 통한 테스트 메서드를 구현한 가짜 클래스를 사용하거나,


익명 내부 클래스를 사용해 테스트하기 원하는 적절한 메서드만이 오류를 발생시키게끔 한다.


깨진 테스트


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

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

문장 중간까지만 써놓고 멈추는 트릭


다시 코딩을 시작 했을때 그 문장을 보고 무슨 생각을 했는지 떠올리기 더 쉽다.


깨끗한 체크인


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

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

체크인 하기전 항상 모든 테스트가 돌아가는 상태로 만들어 두어야 한다.

팀단위 작업에서는 직전까지 무슨일이 있었는지 알지 못하기 때문


28장. 초록막대 패턴

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


실패하는 테스트를 만든 후 첫번째 구현은 어떻게 하는게 좋을까?

상수를 반환하게 하라. 일단 테스트가 통과하면 단계적으로 상수에서 변수로 바꾼다.

왜 하느냐?

뭔가 돌아가는 걸 가진 게 그렇지 않은 것보다 좋고,

몰랐던 오류의 재발견 효과

가짜로 구현하기의 강력한 두가지 효과

  • 심리학적 효과 - 초록막대일때 자신의 위치를 알수 있고, 확신을 가지고 리팩토링 할수 있음.

    • 내가 손봐야 할 곳 명확하게 인지

  • 범위 scope 조절 - 구체적인 예에서 일반화하면 혼동되는 상황을 예방함. 물론 이전 테스트 작동이 보장되면 다음 테스트에 더 집중하게 됨.

    • 사고의 비약 방지

삼각측량


추상화 과정을 테스트로 주도할 때 어떻게 보수적으로 할 수 있을까?

오로지 예가 두개 이상일 때에만 추상화 하라.

규칙이 명확한 삼각측량

명백한 구현


단순한 연산들을 어떻게 구현하는가?

그냥 구현해라.

확신이 든다면 명백한 구현.


실패한다면(빨간막대를 본다면) -> 가짜 구현 or 삼각측량


제대로 구현하면서 깨끗한 코드를 만드는건 비현실적일수 있으므로 확실한 것 부터 차근차근 진행


빨강 / 초록 / 리팩토링의 리듬을 유지

  • 계속 수련하며 명백한 구현과 가짜/삼각측량의 리듬 잡기

하나에서 여럿으로


객체 컬렉션을 다루는 연산은 어떻게 구현하나?

일단 컬렉션 없이 구현하고 그 다음에 컬렉션을 사용

점진적으로 대입하기.


변화 격리하기


ex)

  1. 인자 단일값 하나 받아 구현

  2. 인자에 컬렉션 추가, 테스트 케이스에도 컬렉션 인자 추가

  3. 이후에 사용하지 않는 단일값 삭제후 대신 컬렉션 사용.

코드를 고쳐 테스트 케이스를 바꿔도 코드에 영향이 없도록 하며 테스트 케이스를 개선할수 있다.


29장. xUnit 패턴

단언(assertion)


테스트가 잘 작동하는지 어떻게 검사할 것 인가?

불리언 수식을 작성해서 프로그램이 코드를 판단하도록 하라.

이것은 다음 사항을 의미함

  1. 판단 결과가 불리언 값이어야 한다. 일반적으로 참 값은 모든 테스트가 통과했음을 의미하고, 거짓 값은 뭔가 예상치 못했던 일이 발생했음을 의미한다.

  2. 이 불리언 값은 컴퓨터에 의해 검증되어야 한다.

단언은 구체적이고 명확한 조건을 사용하도록 한다.


픽스처 (setUp)


여러 테스트에서 공통으로 사용하는 객체의 처리는?

각 공통 지역 변수를 인스턴스 변수로 바꾸고 setUp을 정의하여 인스턴스 변수를 초기화 하도록 한다.

픽스처 : 여러 테스트에 걸쳐 동일한 객체


이와 같은 중복은 다음과 같은 이유로 좋지 않다.

  1. 중복된 코드 다수 존재

  2. 인터페이스를 수동으로 변경할 필요가 있을 경우, 여러 테스트를 고쳐주어야 한다.

외부 픽스처(tearDown)


픽스처 중 외부 자원이 있을 경우 어떻게 해제 할 것 인가?

tearDown메서드를 정의 하여 자원을 해제한다.

각 테스트의 목적 중 하나는 테스트가 실행되기 전과 실행된 후의 외부 세계가 동일하게 유지되도록 만드는 것


테스트 중 파일을 열었다면 끝나기 전 닫아야 한다.


테스트 메서드


테스트 케이스 하나를 어떻게 표현할 것인가?

test로 시작하는 이름의 메서드로 나타낸다.

객체지향 언어의 세가지 범주의 구조 계층

  1. 모듈(패키지)

  2. 클래스

  3. 메서드

동일한 픽스처를 공유하는 모든 테스트는 동일한 클래스 계층으로 분리.


각 테스트는 'test'로 시작하도록 메서드 계층으로 분리, 메서드 이름을 잘 짓도록 한다.

  • 테스트 메서드는 의미가 그대로 드러나는 코드로, 읽기 쉬워야 한다.

  • 테스트를 작성하기 전 아웃라인 작성하기 (테스트 작성 전 주석 달기)

    /* 터플 공간(tuple space)에 추가하기. */

예외 테스트


예외가 발생하는 것이 정상인 경우의 테스트는 어떻게 작성 하는가?

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

우리가 원하는 정확한 종류의 예외만을 잡아내야 한다는 점에 유의

단언 - 단언은 구체적이고 명확한 조건을 사용하도록 한다.

전체 테스트


모든 테스트를 한번에 실행하려면?

모든 테스트 슈트에 대한 모음을 작성하면 된다.

IDE가 자동으로 수행해주기 전 환경을 이야기 하는듯..


30장. 디자인 패턴

우리가 언제나 완전히 다른 문제들을 해결하는 것 같지만 우리가 푸는 문제 대다수는 사용하는 도구에 의해 생기는 것

문제를 해결하는 공통의 방법이 존재한다.


디자인 패턴의 엄청난 성공은 객체 프로그래머들이 보는 공통성에 대한 증거다.


30장에서 다룰 패턴에 대한 요약

  • 커맨드:계산 작업에 대한 호출을 메시지가 아닌 객체로 표현한다.

  • 값 객체:객체가 생성된 이후 그 값이 절대로 변하지 안헥 하여 별칭 문제가 발생하지 않게 한다.

  • 널 객체:계산 작업의 기본 사례를 객체로 표현한다.

  • 템플릿 메서드:계산 작업의 ㅂ녀하지 않는 순서를 여러 추상 메서드로 표현한다. 이 추상 메서드들은 상속을 통해 특별한 작업을 수행하게끔 구체화된다.

  • 플러거블 객체:둘 이상의 구현을 객체를 호출함으로써 다양성을 표현한다.

  • 플러거블 셀렉터:객체별로 서로 다른 메서드가 동적으로 호출되게함으로써 필요 없는 하위 클래스의 생성을 피한다.

  • 팩토리 메서드:생성자 대신 메서드를 호출함으로써 객체를 생성한다.

  • 임포스터:현존하는 프롵콜을 갖는 다른 구현을 추가하여 시스템에 변이를 도입한다.

  • 컴포지트:하나의 객체로 여러 객체의 행위 조합을 표현한다.

  • 수집 매개 변수:여러 다른 객체에서 계산한 결과를 모으기 위해 매개 변수를 여러 곳으로 전달한다.

커맨드


간단한 메서드 호출보다 복잡한 형태의 계산 작업에 대한 호출이 필요하다면 어떻게 해야 할까?

계산 작업에 대한 객체를 생성하여 이를 호출하면 된다.

메시지 보내기 : 메서드 호출


메시지 하나를 보내는 것보다 호출이 조금 더 구체적이고 또 조작하기 쉬워지려면, 객체가 해답.


호출 자체를 나타내는 객체를 만들고 계산에 필요한 모든 변수를 초기화 한다.


Runnable 인터페이스가 훌륭한 예시


값 객체


널리 공유해야 하지만 동일성(identity)은 중요하지 않을 때 객체를 어떤 식으로 설계할 수 있을까?

객체가 생성될 때 상태가 변경 되지 않도록 한다. 객체에 대해 수행되는 연산은 새로운 객체를 반환한다.

고전적인 별칭 문제.

  1. 두 객체가 제삼의 다른 객체에 대한 참조를 공유

  2. 한 객체가 공유되는 객체의 상태를 변화

  3. 나머지 다른 객체는 이전 모든 객체와 연관된 계산이 무의미

별칭 문제 해결 방법

  1. 현재 의존하는 객체에 대한 참조를 외부로 알리지 않고, 객체에 대한 복사본을 제공

  2. 옵저버 패턴: 의존하는 객체에 자기를 등록해 놓고, 객체의 상태가 변하면 통지를 받는 방법이다.

모든 값 객체는 동등성을 구현해야 한다.


동일성(identity)와 동등성(equality)

  • 5백원 동전 두 개가 서로 동등할지라도 동일하지는 않다.

널 객체


객체의 특별한 상황을 표현하고자 할때 어떻게 할까?

그 특별한 상황을 표현하는 새로운 객체를 만든다. 그리고 이 객체에 다른 상황을 나타내는 객체와 동일한 프로토콜을 제공한다.

프로토콜이란


인터페이스와 비슷한 뜻이다. 같은 프로토콜을 제공한다는 말의 의미는 같은 인터페이스를 갖거나 해당 클래스의 하위 클래스가 되어야 한다는 뜻이다.

다른 이야기를 하는듯...

null 객체는?


null을 조건으로 확인 하는 대신 null을 표현하는 객체를 만든다.

public void method() {
    Object obj = getObject();
    if (obj != null){
        //객체의 특별한 상황을 표현
    }
}
public getObject() {
    return obj == null ? new NullObject() : obj;
}

public void method() {
    Object obj = getObject();
    //객체의 특별한 상황을 표현
}

템플릿 메서드


작업 순서는 변하지 않지만 각 작업 단위가 변할 수 있다면 어떻게 표현할 것인가?

다른 메서드들을 호출하는 내용으로만 이루어진 메서드를 만든다.

ex)

TestCase
public void runBare() throws Throwable {
    setUp();
    try {
        runTest();
    } finally {
        tearDown();
    }
}

상위 클래스에는 다른 메서드를 호출하는 내용으로만 이루어진 메서드를 만들고


하위 클래스에서는 이 각각의 메서드를 서로 다른 방식으로 구현한다.

각 메서드는 기본 구현을 가지도록 해 아무 것도 처리하지 않는 것도 방법

플러거블 객체


변이를 어떻게 표현할 것인가?

가장 쉬운 건 명시적인 조건문을 사용하는 것이다. 하지만 플러거블 객체가 더 좋다.

명시적 조건문의 단점

  1. 비슷한 소스가 여러곳으로 전파 된다.

  2. 조건이 추가 될 경우 전파된 모든 곳에 추가해야한다.

TDD의 두 번재 수칙은 중복을 제거하는 것으로 이때 플러거블 객체를 사용한다.


명시적인 인터페이스를 사용하는 언어에서는 두 플러거블 객체가 동일한 인터페이스를 구현하게 해야 한다.

  • 중복 조건문을 사용한 경우

SelectionTool
public void mouseDown() {
    selected = findFigure();
    if (selected != null) {
        select(selected);
    }
}

public void mouseMove() {
    if (selected != null) {
        move(selected);
    }
}

public void mouseUp() {
    if (selected == null) {
        selectAll();
    }
}
  • 플러거블 객체를 사용한 경우

SelectionTool

SelectionMode mode;
public void mouseDown() {
    selected = findFigure();
    if (selected != null) {
        mode = SingleSelection(selected);
    } else {
        mode = MultipleSelection();    
    }
}

public void mouseMove() {
    mode.mouseMove();
}

public void mouseUp() {
    mode.mouseUp();
}

플러거블 셀렉터


인스턴스별로 서로 다른 메서드가 동적으로 호출되게 하려면 어떻게 해야 할까?

메서드의 이름을 저장하고 있다가 그 이름에 해당하는 메서드를 동적으로 호출한다.

switch 문을 갖는 하나의 클래스로 작성 할 경우 메서드의 이름이 세 곳에 나뉘어 존재 함

  • 인스턴스 생성하는 곳

  • switch 문

  • 메서드 자체

이는 새로운 출력이 추가 될 시 각각 수정해야 함을 의미한다.

플러거블 셀렉터 해법은 리플랙션을 이용하여 동적으로 메서드를 호출하는 것이다.


직관적인 상황에서 코드를 정리하기 위한 용도로만 플러거블 셀렉터를 사용해야 한다.

메서드를 달랑 한 개만 가지는 하위 클래스들이 한 뭉치나 존재 하는 등

팩토리 메서드


새 객체를 만들 때 유연성을 원하는 경우 객체를 어떻게 생성하는가?

생성자를 쓰는 대신 일반 메서드에서 객체를 생성한다.

메서드를 통해 객체를 생성함으로써 테스트를 변경하지 않을 수 있는 유연함을 얻을 수 있음


하지만 인디렉션으로 인해 명시성이 떨어 지는 단점 존재

메서드는 생성자임을 명시하지 않음

사칭 사기꾼(임포스터)


기존의 코드에 새롱누 변이를 도입하려면 어떻게 해야 할까?

기존의 객체와 같은 프로토콜을 갖지만 구현은 다른 새로운 객체를 추가한다.

리팩토링 중에 나타나는 사칭 사기꾼의 예

  • 널 객체 : 데이터가 없는 상태를 데이터가 있는 상태와 동일하게 취급할 수 있다.

  • 컴포지트 : 객체의 집합을 단일 객체처럼 취급할 수 있다.

올바른 TDD 흐름(빨간 -> 초록 -> 리팩토링)의 중요성을 환기

  1. 임포스터를 사용할 곳을 한번에 집어내는 것은 통찰력이 필요

  2. 리팩토링 중에 사칭 사기꾼을 찾아내는 것은 중복을 제거하는 작업을 통해 유도 됨

  3. 다른 모든 리팩토링 역시 중복을 제거하는 작업에 의해 유도 됨

  4. 올바른 TDD를 익히자

컴포지트


하나의 객체가 다른 객체 목록의 행위를 조합한 것처럼 행동하게 만들려면 어떻게 해야 할까?

객체 집합을 나타내는 객체를 단일 객체에 대한 임포스터로 구현한다.

Account와 Transaction의 예시

  1. Transaction은 값, Account는 값의 합(잔액)

  2. 고객이 여러 계좌를 가지고 있다면?

  3. OverallAccount를 통해 전체 계좌 잔액을 구한다. (X)

    • 중복 발생! (Transaction의 합 = Account, Account의 합 = OverallAccount)

  4. Holding 인터페이스를 통해 Transaction 값을 일반화

  5. Account는 Transaction이 아닌 Holding의 조합(컴포지트)로 재구성

이러한 객체는 실세계의 모습과는 다름


but 설계에 큰 이득을 취할 수 있어 개념적 단절을 용인함


중복을 줄이기 위해 컴포지트를 도입해보자


수집 매개 변수


여러 객체에 걸쳐 존재하는 오퍼레이션의 결과를 수집하려면 어떻게 해야할까?

결과가 수집될 객체를 각 오퍼레이션의 매개 변수로 추가한다.

수집 매개 변수를 추가하는 것은 컴포지트의 일반적인 귀결이다.

  • 23장 수집 매개 변수 예시

    결과가 수집될 객체(TestResult)를 각 오퍼레이션(각 테스트)의 매개 변수로 추가한다.

class TestCaseTest(TestCase):
    def setUp(self):
        self.result = TestResult()

    def testTemplateMethod(self):
        test = WasRun("testMethod")
        test.run(self.result)
        assert "setUp testMethod tearDown " == test.log

    def testResult(self):
        test = WasRun("testMethod")
        result = test.run(self.result)
        assert "1 run, 0 failed" == result.summary()

    def testFailedResult(self):
        test = WasRun("testBrokenMethod")
        result = test.run(self.result)
        assert "1 run, 1 failed" == result.summary()

    def testFailedResultFormatting(self):
        self.result.testStarted()
        self.result.testFailed()
        assert "1 run, 1 failed" == self.result.summary()

    def testSuite(self):
        suite = TestSuite()
        suite.add(WasRun("testMethod"))
        suite.add(WasRun("testBrokenMethod"))
        suite.run(self.result)
        assert "2 run, 1 failed" == self.result.summary()

싱글톤


전역 변수를 제공하지 않는 언어에서 전역 변수를 사용하려면 어떻게 해야 할까?

사용하지 마라.

여러 테스트를 한번에 수행할 때 변수를 공유하기 때문에 문제 발생 가능성 높음

TDD 단위 테스트에 애로사항이 있음


마지막으로 싱글콘 클래스를 사용하는 모듈을 테스트하기 어렵다는 것이다.


단위 테스트를 할때, 단위 테스트는 테스트가 서로 독립적이어야 하며 테스트를 어떤 순서로든 실행 할 수 있어야 하는데, 싱글톤 인스턴스는 자원을 공유하고 있기 때문에, 테스트가 결함없이 수행되려면 매번 인스턴스의 상태를 초기화시켜주어야 한다. 그렇지 않으면 어플리케이션 전역에서 상태를 공유하기 때문에 테스트가 온전하게 수행되지 못할 수도 있다.


많은 테스트 프레임워크가 Mock 객체를 생성할 때 상속에 의존하기 때문에 싱글턴의 클라이언트 코드를 테스트하기 어렵다.


출처 : 싱글톤(Singleton) 패턴 - 꼼꼼하게 알아보자