Post

테스트 주도 개발의 패턴

3. 테스트 주도 개발의 패턴

테스트 주도 개발 패턴

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

테스트(명사)

소프트웨어를 어떻게 테스트할 것인가? -> 자동화된 테스트를 만들어라

스트레스를 많이 받으면 테스트를 점점 더 뜸하게 한다 -> 테스트를 뜸하게 하면 에러가 많아짐 -> 더 많은 스트레스

해결하기 위해 테스트나 자동화된 테스트 필요

격리된 테스트

테스트를 실행하는 것은 서로에게 아무 영향이 없어야 한다

격리된 테스트가 암묵적으로 내포하는 특징 중 하나는 테스트가 실행 순서에 독립적이게 된다는 점

격리된 테스트가 내포하는 게 또 하나 있는데, 이는 주어진 문제를 작은 단위로 분리하기 위해 노력해서 각 테스트를 실행하기 위한 환경을 쉽고 빠르게 세팅할 수 있게해야 한다는 것

테스트를 격리하기 위한 작업은 결과적으로 시스템이 응집도는 높고 결합도는 낮은 객체의 모음으로 구성되도록 함

테스트 목록

시작하기 전에 작성해야 할 테스트 목록을 모두 적어둘 것

  1. 구현해야 할 것들에 대한 테스트를 목록에 적음
  2. 우선 구현할 필요가 있는 모든 오퍼레이션의 사용 예들을 적음
  3. 이미 존재하지 않는 오퍼레이션에 대해서는 해당 오퍼레이션의 널 버전(아무일도 하지 않는 버전)을 리스트에 적음
  4. 깔끔한 코드를 얻기 위해 이번 작업을 끝내기 전에 반드시 해야 할 리팩토링 목록을 적음

테스트 우선

테스트 대상이 되는 코드를 작성하기 직전에 테스트를 작성하는 것이 좋다

테스트를 먼저 하면 스트레스가 줄고, 따라서 테스트를 더 많이 하게 된다.

단언 우선

단언을 제일 먼저 쓰고 시작하라

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

테스트 데이터

테스트를 읽을 떄 쉽고 따라기기 좋을 만한 데이터를 사용

테스트 데이터에 대한 대안은 실제 세상에서 얻어진 실제 데이터를 사용하는 것

명백한 데이터

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

명백한 데이터가 주는 또 다른 이점은 프로그래밍이 더 쉬워진다는 것

빨간 막대 패턴

한 단계 테스트

목록에서 다음 테스트를 고를 때 새로운 무언가를 가르쳐줄 수 있으며, 구현할 수 있다는 확신이 드는 테스트를 고를 것

아는 것에서 모르는 것으로

시작 테스트

오퍼레이션이 아무 일도 하지 않는 경우를 먼저 테스트할 것

설명 테스트

자동화된 테스트를 더 널리 쓰이게 하려면 테스트를 통해 설명을 요청하고 테스트를 통해 설명

TDD 시작법은 테스트를 이용하여 묻고, 테스트를 이용하여 설명하는 것

학습 테스트

외부에서 만든 소프트웨어에 대한 테스트를 작성하기 위해 패키지의 새로운 기능을 처음으로 사용해보기 전에 작성

또 다른 테스트

주제와 무관한 아이디어가 떠오르면 이에 대한 테스트를 할일 목록에 적어놓고 다시 주제로 돌아오면 주제에서 벗어나지 않고 기술적인 논의를 할 수 있다.

회귀 테스트

장애로 인하여 실패하는 테스트, 그리고 통과할 경우엔 장애가 수정되었다고 볼 수 있는 테스트를 가장 간단하게 작성

휴식

지치고 고난에 빠졌을 떈 -> 휴식

때론 어려운 문제에 직면했을 때 계속 압박을 가해서 돌파해야 할 경우도 있다.

다시 하기

길을 잃었다면? -> 다시하기

테스팅 패턴

자식 테스트

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

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

모의 객체

비용이 많이 들거나 복잡한 리소스에 의존하는 객체를 테스트하려면? -> 상수를 반환하게끔 만든 속임수 버전의 리소스를 만들면 된다.

성능과 견고함 외에 모의 객체의 또 다른 가치는 가독성에 있다

모의 객체는 모든 객체의 가시성에 대해 고민하도록 격려해서, 설계에서 커플링이 감소하도록 한다

셀프 션트(자기가 보낸 것이 다시 자신에게 제대로 돌아오는지 확인하는 루프백 테스트와 유사)

테스트 대상이 되는 객체가 원래의 대화 상대가 아니라 테스트 케이스와 대화하도록 만들어 한 객체가 다른 객체와 올바르게 대화하는지 확인

셀프 션트 패턴은 테스트 케이스가 구현할 인터페이스를 얻기 위해 인터페이스 추츨을 해야 한다

로그 문자열

메시지의 호출 순서가 올바른지를 검사

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

크래시 테스트 더미

실제 작업을 수행하는 대신 그냥 에외를 발생시키기만 하는 특수한 객체를 만들어서 에러 테스트

모의 객체와 유사

깨진 테스트

혼자서 프로그래밍할 때 마지막 테스트가 깨진 상태로 끝마치는 것이 좋다

나중에 다시 코딩하기 위해 돌아왔을 때, 어느 잗업부터 시작할 것인지 명백히 알 수 있따.

깨끗한 체크인

팀 프로그래밍을 할 때 프로그래밍 세션을 모든 테스트가 성공한 상태로 끝마치는 것이 좋다

초록 막대 패턴

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

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

일단 테스트가 통과하면 단계적으로 상수를 변수를 사용하는 수식으로 변형

가짜로 구현하기를 강력하게 만드는 두가지 효과

  • 심리학적: 초록 막대 상태에 있는 것은 빨간 막대 상태에 있는 것과 천지차이. 확신을 갖고 리팩토링 가능
  • 범위(scope) 조절: 쓰잘데기 없는 고민으로 때 이르게 혼동하는 일을 예방 가능

삼각측량

추상화 과정에서 테스트로 주도할 때 어떻게 최대한 보수적으로 할 수 있겠는가? -> 오로지 예가 두 개 이살일 때에만 추상화

삼각측량이 매력적인 이유는 그 규칙이 매우 명확하기 때문에

명백한 구현

단순한 연산들은 그냥 구현해 버려라

명백한 구현은 두 번째 기어

만약 손가락이 머리를 따라오지 못하기 시작하면 저속 기어로 전환

하나에서 여럿으로

객체 컬렉션을 다루는 연산? -> 일단은 컬렉션 없이 구현하고 그 다음에 컬렉션을 사용

xUnit 패턴

단언(assertion)

불리언 수식을 작성해서 프로그램이 자동으로 코드가 동작하는지에 대한 판단을 수행하도록 해라

테스트를 완전히 자동화하려면 결과를 평가하는 데 개입되는 인간의 판단을 모조리 끄집어내야 한다

  • 판단 결과가 불리언 값
  • 불리언 값은 컴퓨터에 의해 검증

픽스처

여러 테스트에서 공통으로 사용하는 객체들을 생성할 때 어떻게 하면 좋을까? -> 각 테스트 코드에 있는 지역 변수를 인스턴스 변수로 바꾸고 setUp() 메서드를 재정의하여 이 메서드에서 인스턴스 변수들을 초기화하도록 함

중복이 좋지 않은 이유

  • 복사해서 붙이기를 한다고 하더라도 이런 코드를 반복 작성하는 것엔 시간이 소요되는데, 우리는 테스트를 빨리 작성하길 원한다
  • 인터페이스를 수동으로 변경할 필요가 있을 경우, 여러 테스트를 고쳐주어야 한다

외부 픽스처

픽스처 중 외부 자원이 있을 경우 이를 어떻게 해제? -> tearDown() 메서드를 재정의하여 자원 해제

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

테스트 메서드

테스트 케이스 하나를 표현하기 위해 ‘test’로 시작하는 이름의 메서드로 나타냄

관습에 의해 메서드 이름은 ‘test’로 시작

메서드 이름의 나머지 부분은 나중에 아무것도 모르는 사람이 이 코드를 읽더라고 왜 이 테스트가 작성되었는지 알 수 있도록 단서를 주어야 함

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

예외 테스트

예외가 발생한 것이 정상인 경우에 대한 테스트? -> 예상되는 예외를 잡아서 무시하고, 예외가 발생하지 않은 경우에 한해서 테스트가 실패하게 만든다

전체 테스트

모든 테스트를 한번에 실행하려면? -> 모든 테스트 슈트에 대한 모음 작성

디자인 패턴

커맨드

간단한 메서드 호출보다 복잡한 형태의 계산 작업에 대한 호출이 필요하다면?? -> 계산 작업에 대한 객체를 생성하여 이를 호출

값 객체

널리 공유해야 하지만 동일성은 중요하지 않을 때 객체을 어떤 식으로 설계?? -> 객체가 생성될 때 객체의 상태를 설정한 후 이 상태가 절대 변할 수 없도록 함

이 객체에 대해 수행되는 연산은 언제나 새로운 객체를 반환하게 만든다

두 객체가 제삼의 다른 객체에 대한 참조를 공유하고 있는데, 한 객체가 공유되는 객체의 상태를 변화시키면 나머지 다른 객체는 공유 객체의 상태에 의존하지 않는 편이 차라리 낫다.

별칭 문제 해결 방법

  1. 현재 의존하는 객체에 대한 참조를 결코 외부로 알리지 않는 방법

    대신 복사본 제공

    시간이나 공간 측면에서 비싼 해결책

    공유 객체의 상태 변화를 공유하고 싶은 경우에는 사용할 수 없다

  2. 옵저버 패턴 사용

    의존하는 객체에 자기를 등록해 놓고, 객체의 상태가 변하면 통지를 받는 방법

    제어 흐름을 어렵게 만들 수 있고, 의존성을 설정하고 제거하기 위한 로직이 지저분

  3. 객체를 덜 객체답게 취급

    시간의 흐름에 따라 변하는 상태를 제거

값 객체를 구현할 떄 모든 오퍼레이션은 기존 객체는 변하지 않은 채로 놔두고, 새로운 객체를 반환해야 한다

객체 할당은 퍼포먼스 문제를 야기할 수 있다

모든 값 객체는 동등성을 구현해야 함

널 객체

객체의 특별한 상황을 표현하고자 할 때? -> 그 상황을 표현하는 새로운 객체를 만든다

템플릿 메서드

작업 순서는 변하지 않지만 각 작업 단위에 대한 미래의 개선 사능성을 열어두고 싶은 경우 -> 다른 메서드들을 호출하는 내용으로만 이루어진 메서드를 만든다

플러거블 객체

변이를 어떻게 표현할 것인가? -> 가장 간단한 방법은 명시적인 조건문

TDD의 두 번째 수칙이 중복을 제거하는 것이기 때문에, 명시적인 조건문이 전염되는 싹을 애초에 잘라버려야 한다

단순히 중복을 제거하기 위해 얻은 플러거블 객체는 종종 반직관적

플러거블 셀렉터

인스턴스별로 서로 다른 메서드가 동적으로 호출되게 하려면? -> 메서드의 이름을 저장하고 있다가 그 이름에 해당하는 메서드를 동적으로 호출

확실히 직관적인 상황에서 코드를 정리하기 위한 용도로만 사용해야 한다

팩토리 메서드

새 겍체를 만들 때 유연성을 원하는 경우? -> 생성자를 쓰는 대신 일반 메서드에서 객체를 생성

메서드라는 한 단계의 인디렉션(indirection)을 추가하여 다른 클래스의 인스턴스를 반환할 수 있는 유연함을 얻었다.

팩토리 메서드의 단점은 인디렉션에 있다. 메서드가 생성자처러 생기지는 않았지만 그 안에서 객체를 만든다는 사실을 기억해야 한다

사칭 사기꾼

기존의 코드에 새로운 변이를 도입하려면? -> 기존의 객체와 같은 프로토콜을 갖지만 구현은 다른 새로운 객체를 추가

리팩토링 중에 나타나는 사싱 사기꾼의 두 가지 예

  • 널 객체: 데이터가 없는 상태를 데이터가 있는 상태와 동일하게 취급
  • 컴포지트: 객체의 집합을 단일 객체처럼 취급

컴포지트

하나의 객체가 다른 객체 목록의 행위를 조합한 것처럼 행동하게 만들려면? -> 객체 집합을 나타내는 객체를 단일 객체에 대한 임포스터로 구현

컴포지트 패턴을 적용하는 것은 프로그래머의 트릭이지 세상 사람들에게 일반적으로 받아들여지는 것은 아니다

수집 매개 변수

여러 객체에 걸쳐 존재하는 오퍼레이션의 결과를 수집하려면 어떻게 해야 할까? -> 결과가 수집될 객체를 각 오퍼레이션의 매개 변수로 추가

싱글톤

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

리팩토링

차이점 일치시키기

비슷해 보이는 두 코드 조각을 합치려면? -> 두 코드가 단계적으로 닮아가게끔 수정

  • 두 반복문의 구조가 비슷하다. 이 둘을 동일하게 만들고 나서 하나로 합친다
  • 조건문에 의해 나눠지는 두 분기의 코드가 비슷하다. 이 둘을 동일하게 만들고 나서 조건문을 제거
  • 두 클래스가 비슷하다. 이 둘을 동일하게 만들고 나서 하나를 제거

변화 격리하기

객체나 메서드의 일부만 바꾸려면 어떻게 해야 할까? -> 일단, 바꿔야 할 부분을 격리

일단 바꿀 부분을 격리하고 나서 바꾸는 작업을 수행하면 작업을 되돌리기도 매우 수월하다

데이터 이주시키기

표현 양식을 변경하려면 어떻게? -> 일시적으로 데이터를 중복

방법

  • 새로운 포맷의 인스턴스 변수 추가
  • 기존 포맷의 인스턴스 변수를 세팅하는 모든 부분에서 새로운 인스턴스 변수도 세팅하게 만든다
  • 기존 변수를 사용하는 모든 곳에서 새 변수를 사용하게 만든다
  • 기존 포맷을 제거
  • 새 포맷에 맞게 외부 인터페이스 변경

메서드 추출하기

길고 복잡한 메서드를 읽기 쉽게 만들려면? -> 긴 메서드의 일부분을 별도의 메서드로 분리해내고 이를 호출하게 한다

방법

자동으로 메서드 추출하기

  • 기존의 메서드에서 별도의 메서드로 분리할 수 있을 만한 부분을 찾아낸다.
  • 추출할 영역의 외부에서 선언된 임시 변수에 대해 할당하는 문장이 없는지 확인
  • 추출할 코드를 복사해서 새 코드에 붙인다
  • 원래 메서드에 있던 각각의 임시 변수와 매개 변수 중 새 메서드에서도 쓰이는 게 있으면, 이들을 새 매서드의 매개 변수로 추가
  • 기존의 메서드에서 새 메서드를 호출

이유

복잡한 코드를 이해하고자 할 때

일부 서로 비슷한 내용이 있는 두 메서드에서 중복을 제거하기 위해 사용

메서드 인라인

너무 꼬여있거나 산재한 제어 흐름을 단순화하려면? -> 메서드를 호출하는 부분을 호출될 메서드의 본문으로 교체

방법

  1. 메서드 복사
  2. 메서드 호출하는 부분을 지우고 복사한 코드를 붙임
  3. 모든 형식 매개 변수를 실제 매개 변수로 변경

이유

복잡해서

인터페이스 추출하기

자바 오퍼레이션에 대한 두 번째 구현을 추가하려면?? -> 공통되는 오퍼레이션을 담고 있는 인터페이스를 만든다

방법

  1. 인터페이스 선언
  2. 기존 클래스가 인터페이스를 구현하도록 만든다
  3. 필요한 메서드를 인터페이스에 추가
  4. 가능한 모든 곳의 타입 선언부에서 클래스 이름 대신 인터페이스 이름을 사용하도록 바꾼다

이유

첫 번째 구현에서 두 번째 구현으로 이동

메서드 옮기기

메서드를 원래 있어야 할 장소로 옮기려면? -> 어울리는 클래스에 메서드를 추가해주고, 그것을 호출하게 하라

방법

  1. 메서드를 복사
  2. 원하는 클래스에 붙이고 이름을 적절히 지어준 다음 컴파일
  3. 원래 객체가 메서드 내부에서 참조된다면, 원래 객체를 새 메서드의 매개 변수로 추가
  4. 원래 메서드의 본체를 지우고, 그곳에 새 메서드를 호출하는 코드를 넣는다

메서드 객체

여러개의 매개 변수와 지역 변수를 갖는 복잡한 메서드를 어떻게 표현? -> 메서드를 꺼내서 객체로 만든다

방법

  • 메서드와 같은 매개 변수를 갖는 객체를 만든다
  • 메서의 지역 변수를 객체의 인스턴스 변수로 만든다
  • 원래 메서드와 동일한 내용을 갖는 run()이라는 이름의 메서드를 만든다
  • 원래 메서드에서는 새로 만들어진 클래스의 인스턴스를 생성하고 run()을 호출한다

매개 변수 추가

메서드에 매개 변수를 추가하려면?

방법

  1. 메서드가 인터페이스에 선언되어 있다면 일단 인터페이스에 매개 변수를 추가
  2. 매개 변수를 추가
  3. 컴파일 에러 이용

메서드 매개 변수를 생성자 매개 변수로 바꾸기

하나 이상의 메서드의 매개 변수를 생성자로 옮기려먼 어떻게?

TDD 마스터하기

단계가 얼마나 커야 하나?

  • 각 테스트가 다뤄야 할 범위는 얼마나 넓은가?
  • 리팩토링을 하면서 얼마나 많은 중간 단계를 거쳐야 하는가?

큰 범위와 작은 범위 둘 다 할 수 있어야 함

테스트할 필요가 없는 것은 무엇인가?

두려움이 지루함으로 변할떄까지 테스트를 만들어라

작성하는 것들에 대해서만 테스트

불신할 이유가 없다면 다른 사람이 만든 코드를 테스트 X

좋은 테스트를 갖췄는지의 여부를 어떻게 알 수 있는가?

설계 문제가 있음을 알려주는 테스트의 속성

  • 긴 셋업 코드: 하나의 단순한 단언을 수행하기 위해 수백 줄의 객체 생성 코드가 필요하다면 문제가 있다
  • 셋업 중복: 공통의 셋업 코드를 넣어 둘 공통의 장소를 찾기 힘들다면, 서로 밀접하게 엉킨 객체들이 너무 많다는 뜻
  • 실행 시간이 오래 걸리는 테스트
  • 깨지기 쉬운 테스트

TDD로 프레임워크를 만들려면 어떻게 해야 하나?

코드의 미래에 대해 고려하지 않음으로 인해, 코드가 더 뛰어난 적응성을 가질 수 있게 한다.

테스트 주도 개발은 비록 발생하지 않은 변주 종류들을 잘 표현하는 프레임워크르 만들게 해 준다.

변화에 대응하기 좋음

피드백이 얼마나 필요한가?

경험과 숙고를 통해, 얼마나 많은 테스트를 작성할지 결졍해야 함

테스트를 지워야 할 떄는 언제인가?

  • 첫째 기준은 자신감. 테스트를 삭제할 경우 자신감이 줄어들 것 같으면 절대 지우면 안 된다
  • 둘째 기준은 커뮤니케이션: 두 개의 테스트가 코드의 동일한 부분을 실행하더라도, 이 둘이 서로 다른 시나리오를 말한다면 그대로 남겨두어야 함

프로그래밍 언어나 환경이 TDD에 어떤 영향을 주는가?

TDD 주기를 수행하기가 힘든 언어나 환경에서 작업하게 되면 단계가 커지는 경향이 있다.

거대한 시스템을 개발할 때에도 TDD를 할 수 있는가?

ㅇㅇ

애플리케이션 수준의 테스트로도 개발을 주도할 수 있는가?

기술적인 문제 -> 바로 고정물을 만들어야 함(fixturing)

테스트를 작성하는 것은 사용자(사용자를 포함하는 팀)에게는 기존에 없던 새로운 책임이 됨

테스트와 피드백 사이의 길도 문제임

프로젝트 중반에 TDD를 도입하려면 어떻게 해야할까?

가장 큰 문제는 테스트를 염두에 두지 않고 만든 코드는 테스트하기가 그리 쉽지 않다

코드 전체를 위한 테스트를 한꺼번에 다 만드는 것은 확실히 하지 말아야 함

TDD는 누구를 위한 것?

TDD는 더 나은 코드를 작성한다면 좀더 성공할 것이라는 가정에 근거

TDD는 더 깔끔한 설계를 할 수 있도록, 그리고 더 많은 것을 배워감에 따라 설계를 더 개선할 수 있도록, 적절한 때 적절한 문제에 집중할 수 있게끔 도와줌

TDD는 시간이 지남에 따라 코드에 대한 자신감을 점점 더 쌓아갈 수 있게 해줌

TDD는 초기 조건에 민감한가?

그래 보임

TDD와 패턴의 관계는?

반복적 행동을 규칙으로 환원함으로써 규칙을 적용하는 것은 기계적이며 단순 암기가 된다는 점

어째서 TDD가 잘 작동하는가?

결함 감소 -> 결함을 빨리 발견해 고칠수록 비용은 낮아짐

설계 결정에 대한 피드백 고리를 단축시킴

이름을 테스트 주도 개발이라고 한 이유?

  • 개발: 분석, 논리적 설계, 물리적 설계, 구현, 테스팅, 검토, 통합, 배포를 아우르는 복잡한 춤
  • 주도: 테스트로 주도하니까
  • 테스트: 자동화되고 구체적이며 명확한 테스트

TDD와 익스트림 프로그래밍의 실천법 사이에 어떤 관련이 있는가?

  • 짝 프로그래밍: TDD는 짝 프로그래밍 과정에서 뛰어난 의소소통 수단이 됨
  • 활기차게 일하기: 일을 시작해서 치지면 그만할 것을 권유, 테스트도 동일
  • 지속적인 통합: 테스트는 좀더 자주 통합할 수 있게 해주기 때문에 아주 훌륭한 자원
  • 단순 설계: 테스트를 통과하기 위해 필요한 만큼만 코딩하고 모든 중복을 제거한다면, 자동으로 현재까지의 요구사항에 딱 들어맞는 설계를 얻게 됨
  • 리팩토링: 테스트가 있다면 리팩토링 과정에서 자신감이 생김
  • 지속적인 전달: TDD 테스트들이 시스템을 개선한다면, 고객을 혼란시키지 않으면서도 훨씬 더 자주 코드를 출시할 수 있음

참고

  • 테스트 주도 개발(http://www.yes24.com/Product/Goods/12246033)
This post is licensed under CC BY 4.0 by the author.