테스트 주도 개발로 배우는 객체 지향 설계와 실천 - 1장 테스트 주도 개발의 핵심은 무엇인가?



Intro



초서-독서 한 내용을 그대로 적는 곳이기 때문에 책을 읽지 않은 분들이 보기에 맥락이 애매할 수 있습니다.

초서 : 책을 읽는데 그치는 것이 아닌 손을 이용해 책의 중요한 내용을 옮겨 적음으로써 능동적으로 책의 내용을 수용하고 판단하여 새로운 지식을 재창조하는 과정. 메타인지 학습법




Index


1부 서론


1장 테스트 주도 개발의 핵심은 무엇인가?

학습 과정으로서의 소프트 개발

  • 프로젝트에는 미처 예상하지 못한 요소가 있게 마련이다.
  • 갖가지 중요한 구성 요소가 조합된 시스템은 너무나도 복잡해서 개인이 해당 시스템의 모든 가능성을 이해하기는 어렵다.
  • ‘불확실한 변화를 예측하려면 경험이 늘어남에 따라 불확실성을 해결하는 데 도움이 될 프로세스가 필요하다.’


피드백은 가장 기본적인 도구다

  • 경험에 의거한 피드백
    • 팀에는 반복적인 확동 주기가 필요하다.
    • 각 주기마다 양과 질에 관한 피드백을 받는다.
  • 배포는 현실에서 자신이 내린 가정을 검사할 기회이며, 배포하지 않고는 피드백이 완전해지지 않는다.
  • ‘중첩된 고리형 시스템’ 으로 피드백 주기를 적용하라
    • 짝 프로그래밍, 단위 테스트, 인수 테스트, 일별 회의, 반복 주기, 출시 등…
    • 중첩된 각 고리마다 팀의 산출물이 경험에 의거한 피드백으로 드러나 팀에서는 오류나 오해를 발견하고 수정함.
    • 고리는 서로를 강화함 (안쪽에서 뭔가 모순되는 것이 바깥쪽에서 포착됨)
    • 안쪽 고리는 ‘기술적 세부 사항’ 에 집중 (단위 코드의 역할, 시스템 나머지 부분과의 통합 여부)
    • 바깥쪽 고리는 ‘조직과 팀’ 에 집중
  • 피드백을 일찍 받을수록 좋다
  • 점진적이고 반복적인 개발
    • 점진적인 개발에서는 모든 계층과 구성 요소를 구축한 다음 그것들을 마지막에 통합하는 대신 시스템을 ‘기능별’ 로 구축.
    • 각 기능은 시스템 전 구간에 이르는 ‘조각’ 으로 구현됨.
    • 시스템은 언제나 통합된 상태이며 배포할 준비가 되어 있음.
    • 반복적인 개발은 계속해서 충분한 상태에 이를 때까지 ‘피드백에 응답’‘기능 구현’ 을 다듬는다.


변화를 돕는 실천법

  • 시스템 규모를 점진적으로 키우고, 늘 일어나는 예상치 못한 변화에 대처하기 위해 필요한 두 가지
    • 회귀 오류를 잡아줄 꾸준한 테스트 작성, 테스트 자동화
    • 단순한 코드 유지 (리팩터링)
  • 코드를 작성하기 전에 테스트를 작성한다.
    • 테스트를 ‘설계 활동’ 으로 바꾼다.
    • 테스트를 사용해 ‘코드에서 하고 싶은 바’ 에 관한 생각을 명확하게 한다.
      (‘물리적인 설계’, ‘논리적인 설계’의 분리)
    • 깔끔하고 모듈화된 코드가 생성됨.
    • 빠른 품질 피드백


” 코드 변경에 대한 ‘자신감’ 을 주는 자동화된 회귀 테스트라는 안전망을 구축할 수 있다. “



테스트 주도 개발 간단 정리

TDD 핵심 주기

  1. 테스트 작성
  2. 동작하는 코드 작성
  3. 리팩터링


TDD 혜택

  • 다음 작업에 대한 인수 조건이 명확해짐. (자신이 테스트 코드를 작성하니까 당연함..)
  • 느슨한 구성 요소로 단계별 테스트 가능.
    (격리된 상태 -> 결합된 상태 -> 좀 더 결합된 상태 -> … 점점 더 높은 수준으로)
  • 실행 가능한 설명.
  • 완전한 회귀 스위트가 늘어남


cf) 리펙터링은 작은 규모의 개선 사항을 찾아내는 식으로 진행되는 ‘미시적 기법’이다.
경험상 리팩터링의 ‘여러 작은 단계’ 를 엄격하고 지속적으로 적용해야만 ‘커다란 구조적 개선’ 으로 이어질 수 있다.
주의) 리펙터링은 재설계와 같은 활동이 아니다.



좀 더 큰 그림

  • 기존 ‘레거시’ 에 바로 단위 테스트 작성하여 TDD를 시작하고 싶은 ‘유혹’ 에 빠질 수 있다.
    (없는 것 보단 낫다고 함)
  • 단위 테스트만 있는 프로젝트는 TDD 프로세스가 주는 혜택을 놓칠 수 있다.
    • 아무 데서도 호출하지 않는다. (테스트 자동화 부재?)
    • 시스템의 나머지 부분과 통합할 수 없다.
  • 그렇다면 코드 작성을 어디서 부터 시작할까?


실패하는 테스트

  • 어떤 기능을 구현할 때 인수 테스트를 작성하는 것으로 시작.
    • cf) 인수 테스트 : 만들고자 하는 기능을 시험하는 테스트
    • 인수 테스트를 사용해 작성하려는 코드가 실제로 필요한지 가늠한다. (직접 관련된 코드만 작성)
  • 인수 테스트 하에서 단위 수준의 테스트, 구현, 리팩터링 주기를 따라 기능을 개발.
  • 인수 테스트는 통과 시간이 길다. 아래와 같이 구분한다.
    • ‘현재 작업 중인 인수 테스트’(빌드에 아직 포함 되지 않는)
    • ‘작업을 마친 인수 테스트’(빌드에 포함되며 반드시 통과해야 하는)
  • 실패하는 단위 테스트는 소스 저장소에 절대 커밋해서는 안 된다.


전 구간 테스트

  • 인수 테스트에서는 시스템 내부 코드를 가능한 한 직접 호출하지 말고 ‘시스템 전 구간’ 을 시험해야 한다.
  • 외부 유입 시스템 하고만 상호작용 한다.
    (시스템의 전체적인 작동 방식에는 시스템 외부 환경과의 상호 작용을 포함해야 한다)
  • 단지 외부에서 유래한 시스템과 상호 작용하는 것이라면 ‘경계 간’테스트라고 부르는 편이 더 낫다.
  • 전 구간 테스트는 시스템과 해당 시스템을 구축하고 배포하는 ‘프로세스를 모두 시험’ 하는 방식으로 진행된다.


전 구간 빌드 주기 자동화 (CI,CD)

  • 자동화 주기
    1. 누군가 소스 저장소에 코드 체크인 (수동)
    2. 최신 버전을 체크아웃해서 코드를 컴파일 (이하 자동)
    3. 단위 테스트 실행
    4. 시스템에 통합, 패키지
    5. 운영 환경 수준으로 배포
    6. 외부 접근 지점을 통한 시스템 시험
  • 소프트웨어의 생애 동안 반복적으로 이루어짐.
  • 실제보다 좀 더 어려운 출시 주기가 만들어 질수도 있으므로 전체적인 기술과 조직적인 환경을 이해해야 함.


테스트의 수준

  • 인수 테스트
    • ‘전체 시스템이 동작하는가 ?’
    • 전 구간의 기능이 제대로 동작함을 보증한다.
    • 관련 기술 또는 조직 문화에 따라 차이가 있음.
  • 통합 테스트
    • ‘변경할 수 없는 코드(외부)를 대상으로 코드가 동작하는가 ?’
    • 서드 파티 코드를 대상으로 만든 추상화가 기대한 대로 동작하는지 확인.
    • 영속화 매퍼같은 공용 프레임워크나 조직 내 다른 팀에서 개발한 라이브러리 등..
    • (큰 프로젝트의 경우) 느린 인수 테스트에 비해 좀 더 빠른 피드백을 얻기 위해 통합 테스트가 필요.
    • (작은 프로젝트의 경우) 인수 테스트가 통합 테스트의 역할을 할 수도 있음.
    • 관련 기술 또는 조직 문화에 따라 차이가 있음.
  • 단위 테스트
    • ‘객체가 제대로 동작하는가 ? 객체를 이용하기가 편리한가 ?’
    • (개인)프로그래밍 스타일에 따라 달라짐.
    • 모든 시스템에 공통으로 적용됨.


외부 품질과 내부 품질

  • 외부 품질
    • 시스템이 고객과 사용자의 요구를 얼마나 잘 충족하는가
    • 기능, 신뢰성, 가용성, 응답성 등
  • 내부 품질
    • 시스템이 개발자와 관리자의 요구를 얼마나 잘 충족하는가
    • 이해하기 쉬운가, 변경하기 쉬운가 등 (코드 가독성, 느슨한 설계 등)
    • 내부 품질을 유지하기 위해 시스템 동작 방식을 ‘안전’ 하고 ‘예상 가능한 상태’‘바꿀 수 있게’ 만들어야 한다.
    • 그렇게 해야만 ‘변경’ 으로 인해 ‘큰 규모의 재작업을 해야 할 위험’ 을 최소화할 수 있음.


  • 전 구간 테스트로 시스템 외부 품질을 알 수 있다.
  • 전 구간 테스트를 작성하면 팀 전체가 도메인을 얼마나 잘 이해하는지 알 수 있다.
  • 단위 테스트로 코드를 얼마나 잘 작성했는지 알 수 있다. (전 구간 테스트로는 알 수 없음)


단위 테스트

  • 철저한 단위 테스트는 내부 품질을 개선하는데 도움이 된다.
  • 단위를 테스트하려면 ‘테스트 픽스처’ 에서 해당 단위를 시스템 바깥에서 실행할 수 있게 ‘구조화’ 해야 하기 때문.
  • 객체에 대한 단위 테스트 하려면
    • 객체를 생성하고 해당 객체의 의존성을 제공하며, 객체와 상호 작용하고, 예상대로 동작하는지 검사.
    • 클래스가 대체할 수 있는 명시적인 의존성(=느슨한 결합)과 명확한 책임(=높은 응집력)을 지녀야 한다.
    • 설계를 잘못하면 (클래스가 멀리 떨어져 있는 시스템의 일부와 긴밀하게 결합 | 암시적인 의존성 | 불분명한 책임이 많음 등)
      ‘단위 테스트를 작성하거나 이해하기 어려워짐’
    • 테스트에 귀 기울이기
      • ‘테스트 하기 싫다’ ↓
      • ‘왜? 하기 싫을까’ (작성하기 어려운 이유 조사) ↓
      • 도메인 재설계(설계 방식이 틀리진 않았을까) | 코드 구조 개선(리팩터링)


테스트 픽스처 란

  • cf) 테스트 픽스처란 : SUT(System Under Test)를 실행하기 위해 필요한 모든 것.
  • 픽스처는 테스트에 필요한 자원 생성, 테스트 가능한 상태로 세팅. (픽스처 설치 단계에 해당)
  • 픽스처는 테스트 ‘선조건’ 을 의미, 이를 구현한 클래스는 Test Case Class 이다.
    • 1회용 신선한 픽스처
      • 테스트가 실행될 때마다 새로운 픽스처 생성.
        (다른 테스트와 의존 관계가 없는 완벽한 독립성 보장, 대신 느림)
    • 지속되는 픽스처
      • 테스트 대상 컴포넌트가 ‘어떤 상태를 유지 시키는 매커니즘을 가진 것’과 강하게 결합되어 있을 때 지속되는 픽스처가 될 수 있다.
      • 테스트 수행마다 해체 코드를 실행하여 ‘지속되는 신선한 픽스처’로 변경 가능
      • 픽스처를 지속되게 만드는 요소
        • 데이터 베이스 사용
        • 클래스 변수에 데이터 저장
        • 다른 테스트에서 사용되는 1회용 픽스처를 수행 후에도 Test Case Class 에서 갖고 있는 경우
    • 공유 픽스처
      • 테스트 실행 속도를 향상시키기 위해 사용.
        많은 테스트가 하나의 픽스처를 공유하여 재사용.
      • 오래된 픽스처는 어질러지는 부수효과 발생, 이로인해 반복 안되는 테스트, 테스트 실행 전쟁 등 문제 발생.

        테스트 실행 전쟁 : 각 테스트에서 동시에 같은 픽스처 자원 접근, 테스트가 무작위로 실패하게 되는 문제.