🚀
Incheol's TECH BLOG
  • Intro
  • Question & Answer
    • JAVA
      • JVM
      • String, StringBuffer, StringBuilder
      • JDK 17일 사용한 이유(feat. JDK 8 이후 훑어보기)
      • 스택 오버 플로우(SOF)
      • 블럭킹 | 논블럭킹 | 동기 | 비동기
      • 병렬처리를 이용한 이미지 리사이즈 개선
      • heap dump 분석하기 (feat. OOM)
      • G1 GC vs Z GC
      • JIT COMPILER
      • ENUM
      • STATIC
      • Thread(쓰레드)
      • hashCode()와 equals()
      • JDK 8 특징
      • break 와 continue 사용
      • STREAM
      • Optional
      • 람다와 클로저
      • Exception(예외)
      • Garbage Collector
      • Collection
      • Call by Value & Call by Reference
      • 제네릭(Generic)
    • SPRING
      • Spring 특징
      • N+1 문제
      • 테스트 코드 어디까지 알아보고 오셨어요?
      • 테스트 코드 성능 개선기
      • RestTemplate 사용시 주의사항
      • 동시성 해결하기(feat. TMI 주의)
      • redisson trylock 내부로직 살펴보기
      • DB 트래픽 분산시키기(feat. Routing Datasource)
      • OSIV
      • @Valid 동작 원리
      • mybatis @Builder 주의사항
      • 스프링 클라우드 컨피그 갱신 되지 않는 이슈(feat. 서비스 디스커버리)
      • ImageIO.read 동작하지 않는 경우
      • 카프카 transaction 처리는 어떻게 해야할까?
      • Spring Boot 특징
      • Spring 5 특징
      • JPA vs MyBatis
      • Filter와 Interceptor
      • 영속성 컨텍스트(Persistence Context)
      • @Transactional
      • @Controlleradvice, @ExceptionHandler
      • Spring Security
      • Dispatcher Servlet
      • @EnableWebMvc
      • Stereo Type(스테레오 타입)
      • AOP
      • JPA Repository 규칙
    • DATABASE
      • Database Index
      • SQL vs NoSQL
      • DB 교착상태
      • Isolation level
      • [MySQL] 이모지 저장은 어떻게 하면 좋을까?
      • SQL Hint
      • JOIN
    • INFRA
      • CLOUD COMPUTING
      • GIT
      • DOCKER
      • 카프카 찍먹하기 1부
      • 카프카 찍먹하기 2부 (feat. 프로듀서)
      • 카프카 찍먹하기 3부 (feat. 컨슈머)
      • JENKINS
      • POSTMAN
      • DNS 동작 원리
      • ALB, NLB,ELB 차이는?
      • 카프카 파티션 주의해서 사용하자
      • DEVOPS
      • JWT
      • OSI 7 Layer
      • MSA
      • 서비스 디스커버리는 어떻게 서비스 등록/해제 하는걸까?
      • 핀포인트 사용시 주의사항!! (feat 로그 파일 사이즈)
      • AWS EC2 도메인 설정 (with ALB)
      • ALB에 SSL 설정하기(feat. ACM)
      • 람다를 활용한 클라우드 와치 알림 받기
      • AWS Personalize 적용 후기… 😰
      • CloudFront를 활용한 S3 성능 및 비용 개선
    • ARCHITECTURE
      • 객체지향과 절차지향
      • 상속보단 합성
      • SOLID 원칙
      • 캡슐화
      • DDD(Domain Driven Design)
    • COMPUTER SCIENCE
      • 뮤텍스와 세마포어
      • Context Switch
      • REST API
      • HTTP HEADER
      • HTTP METHOD
      • HTTP STATUS
    • CULTURE
      • AGILE(Feat. 스크럼)
      • 우리는 성장 할수 있을까? (w. 함께 자라기)
      • Expert Beginner
    • SEMINAR
      • 2022 INFCON 후기
        • [104호] 사이드 프로젝트 만세! - 기술만큼 중요했던 제품과 팀 성장기
        • [102호] 팀을 넘어서 전사적 협업 환경 구축하기
        • [103호] 코드 리뷰의 또 다른 접근 방법: Pull Requests vs. Stacked Changes
        • [105호] 실전! 멀티 모듈 프로젝트 구조와 설계
        • [105호] 지금 당장 DevOps를 해야 하는 이유
        • [102호] (레거시 시스템) 개편의 기술 - 배달 플랫폼에서 겪은 N번의 개편 경험기
        • [102호] 서버비 0원, 클라우드 큐 도입으로 해냈습니다!
  • STUDY
    • 오브젝트
      • 1장 객체, 설계
      • 2장 객체지향 프로그래밍
      • 3장 역할, 책임, 협력
      • 4장 설계 품질과 트레이드 오프
      • 5장 책임 할당하기
      • 6장 메시지와 인터페이스
      • 7징 객체 분해
      • 8장 의존성 관리하기
      • 9장 유연한 설계
      • 10장 상속과 코드 재사용
      • 11장 합성과 유연한 설계
      • 12장 다형성
      • 13장 서브클래싱과 서브타이핑
      • 14장 일관성 있는 협력
      • 15장 디자인 패턴과 프레임워크
      • 마무리
    • 객체지향의 사실과 오해
      • 1장 협력하는 객체들의 공동체
      • 2장 이상한 나라의 객체
      • 3장 타입과 추상화
      • 4장 역할, 책임, 협력
    • JAVA ORM JPA
      • 1장 JPA 소개
      • 2장 JPA 시작
      • 3장 영속성 관리
      • 4장 엔티티 매핑
      • 5장 연관관계 매핑 기초
      • 6장 다양한 연관관계 매핑
      • 7장 고급 매핑
      • 8장 프록시와 연관관계 관리
      • 9장 값 타입
      • 10장 객체지향 쿼리 언어
      • 11장 웹 애플리케이션 제작
      • 12장 스프링 데이터 JPA
      • 13장 웹 애플리케이션과 영속성 관리
      • 14장 컬렉션과 부가 기능
      • 15장 고급 주제와 성능 최적화
      • 16장 트랜잭션과 락, 2차 캐시
    • 토비의 스프링 (3.1)
      • 스프링의 이해와 원리
        • 1장 오브젝트와 의존관계
        • 2장 테스트
        • 3장 템플릿
        • 4장 예외
        • 5장 서비스 추상화
        • 6장 AOP
        • 8장 스프링이란 무엇인가?
      • 스프링의 기술과 선택
        • 5장 AOP와 LTW
        • 6장 테스트 컨텍스트 프레임워크
    • 클린코드
      • 1장 깨끗한 코드
      • 2장 의미 있는 이름
      • 3장 함수
      • 4장 주석
      • 5장 형식 맞추기
      • 6장 객체와 자료 구조
      • 9장 단위 테스트
    • 자바 트러블슈팅(with scouter)
      • CHAP 01. 자바 기반의 시스템에서 발생할 수 있는 문제들
      • CHAP 02. scouter 살펴보기
      • CHAP 03. scouter 설정하기(서버 및 에이전트)
      • CHAP 04. scouter 클라이언트에서 제공하는 기능들
      • CHAP 05. scouter XLog
      • CHAP 06. scouter 서버/에이전트 플러그인
      • CHAP 07. scouter 사용 시 유용한 팁
      • CHAP 08. 스레드 때문에(스레드에서) 발생하는 문제들
      • CHAP 09. 스레드 단면 잘라 놓기
      • CHAP 10. 잘라 놓은 스레드 단면 분석하기
      • CHAP 11. 스레드 문제
      • CHAP 12. 메모리 때문에 발생할 수 있는 문제들
      • CHAP 13. 메모리 단면 잘라 놓기
      • CHAP 14. 잘라 놓은 메모리 단면 분석하기
      • CHAP 15. 메모리 문제(Case Study)
      • CHAP 24. scouter로 리소스 모니터링하기
      • CHAP 25. 장애 진단은 이렇게 한다
      • 부록 A. Fatal error log 분석
      • 부록 B. 자바 인스트럭션
    • 테스트 주도 개발 시작하기
      • CHAP 02. TDD 시작
      • CHAP 03. 테스트 코드 작성 순서
      • CHAP 04. TDD/기능 명세/설계
      • CHAP 05. JUnit 5 기초
      • CHAP 06. 테스트 코드의 구성
      • CHAP 07. 대역
      • CHAP 08. 테스트 가능한 설계
      • CHAP 09. 테스트 범위와 종류
      • CHAP 10. 테스트 코드와 유지보수
      • 부록 A. Junit 5 추가 내용
      • 부록 C. Mockito 기초 사용법
      • 부록 D. AssertJ 소개
    • KOTLIN IN ACTION
      • 1장 코틀린이란 무엇이며, 왜 필요한가?
      • 2장 코틀린 기초
      • 3장 함수 정의와 호출
      • 4장 클래스, 객체, 인터페이스
      • 5장 람다로 프로그래밍
      • 6장 코틀린 타입 시스템
      • 7장 연산자 오버로딩과 기타 관례
      • 8장 고차 함수: 파라미터와 반환 값으로 람다 사용
      • 9장 제네릭스
      • 10장 애노테이션과 리플렉션
      • 부록 A. 코틀린 프로젝트 빌드
      • 부록 B. 코틀린 코드 문서화
      • 부록 D. 코틀린 1.1과 1.2, 1.3 소개
    • KOTLIN 공식 레퍼런스
      • BASIC
      • Classes and Objects
        • Classes and Inheritance
        • Properties and Fields
    • 코틀린 동시성 프로그래밍
      • 1장 Hello, Concurrent World!
      • 2장 코루틴 인 액션
      • 3장 라이프 사이클과 에러 핸들링
      • 4장 일시 중단 함수와 코루틴 컨텍스트
      • 5장 이터레이터, 시퀀스 그리고 프로듀서
      • 7장 스레드 한정, 액터 그리고 뮤텍스
    • EFFECTIVE JAVA 3/e
      • 객체 생성과 파괴
        • 아이템1 생성자 대신 정적 팩터리 메서드를 고려하라
        • 아이템2 생성자에 매개변수가 많다면 빌더를 고려하라
        • 아이템3 private 생성자나 열거 타입으로 싱글턴임을 보증하라
        • 아이템4 인스턴스화를 막으려거든 private 생성자를 사용하라
        • 아이템5 자원을 직접 명시하지 말고 의존 객체 주입을 사용하라
        • 아이템6 불필요한 객체 생성을 피하라
        • 아이템7 다 쓴 객체 참조를 해제하라
        • 아이템8 finalizer와 cleaner 사용을 피하라
        • 아이템9 try-finally보다는 try-with-resources를 사용하라
      • 모든 객체의 공통 메서드
        • 아이템10 equals는 일반 규약을 지켜 재정의하라
        • 아이템11 equals를 재정의 하려거든 hashCode도 재정의 하라
        • 아이템12 toString을 항상 재정의하라
        • 아이템13 clone 재정의는 주의해서 진행해라
        • 아이템14 Comparable을 구현할지 고려하라
      • 클래스와 인터페이스
        • 아이템15 클래스와 멤버의 접근 권한을 최소화하라
        • 아이템16 public 클래스에서는 public 필드가 아닌 접근자 메서드를 사용하라
        • 아이템17 변경 가능성을 최소화하라
        • 아이템18 상속보다는 컴포지션을 사용하라
        • 아이템19 상속을 고려해 설계하고 문서화하라. 그러지 않았다면 상속을 금지하라
        • 아이템20 추상 클래스보다는 인터페이스를 우선하라
        • 아이템21 인터페이스는 구현하는 쪽을 생각해 설계하라
        • 아이템22 인터페이스 타입을 정의하는 용도로만 사용하라
        • 아이템23 태그 달린 클래스보다는 클래스 계층구조를 활용하라
        • 아이템24 멤버 클래스는 되도록 static으로 만들라
        • 아이템25 톱레벨 클래스는 한 파일에 하나만 담으라
      • 제네릭
        • 아이템26 로 타입은 사용하지 말라
        • 아이템27 비검사 경고를 제거하라
        • 아이템28 배열보다는 리스트를 사용하라
        • 아이템29 이왕이면 제네릭 타입으로 만들라
        • 아이템30 이왕이면 제네릭 메서드로 만들라
        • 아이템31 한정적 와일드카드를 사용해 API 유연성을 높이라
        • 아이템32 제네릭과 가변인수를 함께 쓸 때는 신중하라
        • 아이템33 타입 안전 이종 컨테이너를 고려하라
      • 열거 타입과 애너테이션
        • 아이템34 int 상수 대신 열거 타입을 사용하라
        • 아이템35 ordinal 메서드 대신 인스턴스 필드를 사용하라
        • 아이템36 비트 필드 대신 EnumSet을 사용하라
        • 아이템37 ordinal 인덱싱 대신 EnumMap을 사용하라
        • 아이템38 확장할 수 있는 열거 타입이 필요하면 인터페이스를 사용하라
        • 아이템 39 명명 패턴보다 애너테이션을 사용하라
        • 아이템40 @Override 애너테이션을 일관되게 사용하라
        • 아이템41 정의하려는 것이 타입이라면 마커 인터페이스를 사용하라
      • 람다와 스트림
        • 아이템46 스트림에는 부작용 없는 함수를 사용하라
        • 아이템47 반환 타입으로는 스트림보다 컬렉션이 낫다
        • 아이템48 스트림 병렬화는 주의해서 적용하라
      • 메서드
        • 아이템49 매개변수가 유효한지 검사하라
        • 아이템50 적시에 방어적 본사본을 만들라
        • 아이템53 가변인수는 신중히 사용하라
        • 아이템 54 null이 아닌, 빈 컬렉션이나 배열을 반환하라
        • 아이템56 공개된 API 요소에는 항상 문서화 주석을 작성하라
      • 일반적인 프로그래밍 원칙
        • 아이템56 공개된 API 요소에는 항상 문서화 주석을 작성하라
        • 아이템57 지역변수의 범위를 최소화하라
        • 아이템 60 정확한 답이 필요하다면 float와 double은 피하라
      • 예외
        • 아이템 73 추상화 수준에 맞는 예외를 던지라
        • 아이템 74 메서드가 던지는 모든 예외를 문서화하라
      • 동시성
        • 아이템78 공유 중인 가변 데이터는 동기화해 사용하라
        • 아이템79 과도한 동기화는 피하라
        • 아이템 80 스레드보다는 실행자, 태스크, 스트림을 애용하라
      • 직렬화
        • 아이템 87 커스텀 직렬화 형태를 고려해보라
    • Functional Programming in Java
      • Chap 01. 헬로, 람다 표현식
      • Chap 02. 컬렉션의 사용
      • Chap 03. String, Comparator, 그리고 filter
      • Chap 04. 람다 표현식을 이용한 설계
      • CHAP 05. 리소스를 사용한 작업
      • CHAP 06. 레이지
      • CHAP 07. 재귀 호출 최적화
      • CHAP 08. 람다 표현식의 조합
      • CHAP 09. 모든 것을 함께 사용해보자
      • 부록 1. 함수형 인터페이스의 집합
      • 부록 2. 신택스 오버뷰
    • 코틀린 쿡북
      • 2장 코틀린 기초
      • 3장 코틀린 객체지향 프로그래밍
      • 4장 함수형 프로그래밍
      • 5장 컬렉션
      • 6장 시퀀스
      • 7장 영역 함수
      • 9장 테스트
      • 10장 입력/출력
      • 11장 그 밖의 코틀린 기능
    • DDD START!
      • 1장 도메인 모델 시작
      • 2장 아키텍처 개요
      • 3장 애그리거트
      • 4장 리포지터리와 모델구현(JPA 중심)
      • 5장 리포지터리의 조회 기능(JPA 중심)
      • 6장 응용 서비스와 표현 영역
      • 7장 도메인 서비스
      • 8장 애그리거트 트랜잭션 관리
      • 9장 도메인 모델과 BOUNDED CONTEXT
      • 10장 이벤트
      • 11장 CQRS
    • JAVA 8 IN ACTION
      • 2장 동작 파라미터화 코드 전달하기
      • 3장 람다 표현식
      • 4장 스트림 소개
      • 5장 스트림 활용
      • 6장 스트림으로 데이터 수집
      • 7장 병렬 데이터 처리와 성능
      • 8장 리팩토링, 테스팅, 디버깅
      • 9장 디폴트 메서드
      • 10장 null 대신 Optional
      • 11장 CompletableFuture: 조합할 수 있는 비동기 프로그래밍
      • 12장 새로운 날짜와 시간 API
      • 13장 함수형 관점으로 생각하기
      • 14장 함수형 프로그래밍 기법
    • 객체지향과 디자인패턴
      • 객체 지향
      • 다형성과 추상 타입
      • 재사용: 상속보단 조립
      • 설계 원칙: SOLID
      • DI와 서비스 로케이터
      • 주요 디자인 패턴
        • 전략패턴
        • 템플릿 메서드 패턴
        • 상태 패턴
        • 데코레이터 패턴
        • 프록시 패턴
        • 어댑터 패턴
        • 옵저버 패턴
        • 파사드 패턴
        • 추상 팩토리 패턴
        • 컴포지트 패턴
    • NODE.JS
      • 1회차
      • 2회차
      • 3회차
      • 4회차
      • 6회차
      • 7회차
      • 8회차
      • 9회차
      • 10회차
      • 11회차
      • 12회차
      • mongoose
      • AWS란?
    • SRPING IN ACTION (5th)
      • Chap1. 스프링 시작하기
      • Chap 2. 웹 애플리케이션 개발하기
      • Chap 3. 데이터로 작업하기
      • Chap 4. 스프링 시큐리티
      • Chap 5. 구성 속성 사용하기
      • Chap 6. REST 서비스 생성하기
      • Chap 7. REST 서비스 사용하기
      • CHAP 8 비동기 메시지 전송하기
      • Chap 9. 스프링 통합하기
      • CHAP 10. 리액터 개요
      • CHAP 13. 서비스 탐구하기
      • CHAP 15. 실패와 지연 처리하기
      • CHAP 16. 스프링 부트 액추에이터 사용하기
    • 스프링부트 코딩 공작소
      • 스프링 부트를 왜 사용 해야 할까?
      • 첫 번째 스프링 부트 애플리케이션 개발하기
      • 구성을 사용자화 하기
      • 스프링부트 테스트하기
      • 액추에이터로 내부 들여다보기
    • ANGULAR 4
      • CHAPTER 1. A gentle introduction to ECMASCRIPT 6
      • CHAPTER 2. Diving into TypeScript
      • CHAPTER 3. The wonderful land of Web Components
      • CHAPTER 4. From zero to something
      • CHAPTER 5. The templating syntax
      • CHAPTER 6. Dependency injection
      • CHAPTER 7. Pipes
      • CHAPTER 8. Reactive Programming
      • CHAPTER 9. Building components and directives
      • CHAPTER 10. Styling components and encapsulation
      • CHAPTER 11. Services
      • CHAPTER 12. Testing your app
      • CHAPTER 13. Forms
      • CHAPTER 14. Send and receive data with Http
      • CHAPTER 15. Router
      • CHAPTER 16. Zones and the Angular magic
      • CHAPTER 17. This is the end
    • HTTP 완벽 가이드
      • 게이트웨이 vs 프록시
      • HTTP Header
      • REST API
      • HTTP Method 종류
        • HTTP Status Code
      • HTTP 2.x
  • REFERENCE
    • TECH BLOGS
      • 어썸데브블로그
      • NAVER D2
      • 우아한 형제들
      • 카카오
      • LINE
      • 스포카
      • 티몬
      • NHN
      • 마켓컬리
      • 쿠팡
      • 레진
      • 데일리 호텔
      • 지그재그
      • 스타일쉐어
      • 구글
      • 야놀자
    • ALGORITHM
      • 생활코딩
      • 프로그래머스
      • 백준
      • 알고스팟
      • 코딜리티
      • 구름
      • 릿코드
Powered by GitBook
On this page
  • 테스트 코드 작성 순서
  • 초반에 복잡한 테스트부터 시작하면 안 되는 이유
  • 구현하기 쉬운 테스트부터 시작하기
  • 예외 상황을 먼저 테스트해야 하는 이유
  • 완급 조절
  • 지속적인 리팩토링
  • 테스트 작성 순서 연습
  • 쉬운 것부터 테스트
  • 코드 정리:중복 제거
  • 예외 상황 처리
  • 다음 테스트 선택: 다시 예외 상황
  • 다음 테스트를 추가하기 전에 리팩토링
  • 예외 상황 테스트 진행 계속
  • 코드 정리: 상수를 변수로
  • 다음 테스트 선택: 쉬운 테스트
  • 예외 상황 테스트 추가
  • 시작이 안 될 때는 단언부터 고민
  • 구현이 막히면

Was this helpful?

  1. STUDY
  2. 테스트 주도 개발 시작하기

CHAP 03. 테스트 코드 작성 순서

테스트 주도 개발 시작하기 3장을 요약한 내용입니다.

테스트 코드 작성 순서

2장에서 작성한 암호 측정 기능을 TDD로 구현하였다. 이를 토대로 테스트 코드를 작성한 순서는 다음과 같았다.

  1. 모든 규칙을 충족하는 암호 강도는 '강함'

  2. 길이만 8글자 미만이고 나머지 규칙은 충족하는 암호의 강도는 '보통'

  3. 숫자를 포함하지 않고 나머지 규칙은 충족하는 암호의 강도는 '보통'

  4. 값이 없는 암호의 강도는 '유효하지 않음'

  5. 대문자를 포함하지 않고 나머지 규칙은 충족하는 경우

  6. 길이가 8글자 이상인 규칙만 충족하는 경우

  7. 숫자 포함 규칙만 충족하는 경우

  8. 대문자 포함 규칙만 충족하는 경우

  9. 아무 규칙도 충족하지 않은 경우

이 순서가 그냥 나온 것은 아니다. 실제로 이 순서는 다음 규칙에 따라 나왔다.

  • 쉬운 경우에서 어려운 경우로 진행

  • 예외적인 경우에서 정상인 경우로 진행

초반에 복잡한 테스트부터 시작하면 안 되는 이유

  • 만약 초반부터 다양한 조합을 검사하는 복잡한 상황을 테스트로 추가한다고 가정해보자

    • 대문자 포함 규칙만 충족하는 경우

    • 모든 규칙을 충족하는 경우

    • 숫자를 포함하지 않고 나머지 규칙은 충족하는 경우

  • 이 순서대로 TDD를 진행해 보자

  • 대문자 포함 규칙만 충족하는 경우를 테스트하기 위한 코드를 작성해보자

    @Test
    void meetsOnlyUpperCriteria_Then_Weak() {
      PasswordStrengthMeter meter = new PasswordStrengthMeter();
    	PasswordStrength result = meter.meter("abcDef");
      assertStrength(result, PasswordStrength.WEAK);
    }
  • 이 테스트에 대한 구현은 간단하다. 단순히 WEAK를 리턴하면 된다.

    public PasswordStrength meter(String s) {
        return PasswordStrength.WEAK;
    }
  • 이제 모든 규칙을 충족하는 경우를 테스트하기 위한 코드를 추가할 차례다.

    @Test
    void meetsAllCriteria_Then_Weak() {
      PasswordStrengthMeter meter = new PasswordStrengthMeter();
    	PasswordStrength result = meter.meter("abcDef12");
      assertStrength(result, PasswordStrength.STRONG);
    }
  • 이 테스트를 가장 빨리 통과시킬 수 있는 방법은 입력값이 "abcDef12"이면 STRONG을 리턴하는 코드를 추가하는 것이다.

    public PasswordStrength meter(String s) {
    		if("abcDef12".equals(s)) return PasswordStrength.STRONG;
        return PasswordStrength.WEAK;
    }
  • 테스트 예를 하나 더 추가해보자

    @Test
    void meetsAllCriteria_Then_Weak() {
      PasswordStrengthMeter meter = new PasswordStrengthMeter();
    	PasswordStrength result = meter.meter("abcDef12");
      assertStrength(result, PasswordStrength.STRONG);
    
    	PasswordStrength result = meter.meter("aZcDef12");
      assertStrength(result, PasswordStrength.STRONG);
    }
  • 검증 예를 추가할때마다 if 절을 늘릴 수는 없다. 좀 더 범용적인 구현이 필요하다.

범용적인 구현은 어떤 모습일까?

  • 모든 조건을 충족하는지 검사하는 코드일까?

  • 일단 대문자만 포함하면 STRONG을 리턴하는 코드여야 할까?

  • 아니면 8글자 이상이면 STRONG을 리턴하는 코드여야 할까?

  • 두 번째 테스트를 추가한 것뿐인데 벌써부터 막히기 시작한다.

  • 두 번째 테스트를 통과시키려면 모든 규칙을 확인하는 코드를 구현해야 할것만 같다.

보통의 개발자는 한 번에 많은 코드를 만들다 보면 나도 모르게 버그를 만들고 나중에 버그를 잡기 위해 많은 시간을 허비하게 된다. 당연히 테스트 통과 시간도 길어진다. 그뿐만 아니라 코드 작성 시간이 길어지면 집중력도 떨어져서 흐름이 자주 끊기게 된다.

구현하기 쉬운 테스트부터 시작하기

  • 가장 구현하기 쉬운 경우부터 시작하면 빠르게 테스트를 통과시킬 수 있다.

  • 암호 강도 측정 예에서는 어떤 것이 가장 쉬울까?

    • 모든 조건을 충족하는 경우

    • 모든 조건을 충족하지 않는 경우

  • 모든 조건을 충족하는 경우를 첫 번째 테스트로 시작했다.

    • 구현 코드를 단순히 STRONG으로 리턴하여 해당 테스트 케이스를 통과하였다.

  • 다음으로 어떤 테스트를 추가할까? 역시 구현하기 쉬운 것이 선택 기준이 된다.

    • 모든 규칙을 충족하지 않는 경우

      • 정반대 조건을 테스트하려면 결국 모든 규칙을 검사하는 코드를 구현해야 할 것 같다.

    • 한 규칙만 충족하는 경우

      • 한 규칙을 충족하는지 여부를 검사해서 WEAK를 리턴하면 된다.

    • 두 규칙을 충족하는 경우

      • 이 경우에도 한 규칙을 충족하는지 여부를 검사하면 될 것 같다.

      • 두 규칙을 충족한다는 것은 충족하지 않는 규칙이 하나 존재한다는 것이므로 한 규칙을 충족하는지 검사해서 충족하지 않으면 NORMAL을 리턴하면 된다.

  • 이렇게 하나의 테스트를 통과했으면 그다음으로 구현하기 쉬운 테스트를 선택해야 한다. 구현이 어렵지 않기 때문에 짧은 시간에 구현을 완료하고 테스트를 통과시킬 수 있다. 이를 통해 점진적으로 구현을 완성해 나갈 수 있다.

  • 한 번에 구현하는 시간이 짧아지면 디버깅할 때에 유리하다.

예외 상황을 먼저 테스트해야 하는 이유

  • 다양한 예외 상황은 복잡한 if-else 블록을 동반할 때가 많다.

  • 예외 상황을 전혀 고려하지 않은 코드에 예외 상황을 반영하려면 코드의 구조를 뒤집거나 코드 중간에 예외 상황을 처리하기 위해 조건문을 중복해서 추가하는 일이 벌어진다. (버그 발생 가능성을 높인다)

  • TDD를 하는 동안 예외 상황을 찾고 테스트에 반영하면 예외 상황을 처리하지 않아 발생하는 버그도 줄여준다.

  • 사소한 버그가 서비스 중단을 일으킬 수 있다.

완급 조절

  • TDD로 구현할 때 어려운 것 줄 하나는 한 번에 얼마만큼의 코드를 작성할 것인가이다.

  • TDD를 처음 접할 때는 다음 단계에 따라 TDD를 익혀보자

    1. 정해진 값을 리턴

    2. 값 비교를 이용해서 정해진 값을 리턴

    3. 다양한 테스트를 추가하면서 구현을 일반화

  • 위 단계를 이용해서 TDD를 연습한 개발자는 조금씩 기능을 구현해 나갈 수 있다.

  • TDD가 익숙해지면 상황에 따라 속도를 조절할 수 있게 된다.

지속적인 리팩토링

  • 테스트를 통과한 뒤에는 리팩토링을 진행한다.

  • 코드 중복은 대표적인 리팩토링 대상이다.

  • TDD를 진행하는 과정에서 지속적으로 리팩토링을 진행하면 코드 가독성이 높아진다.

일단 동작하는 코드를 만드는 능력은 중요하다. 하지만 소프트웨어의 생존 시간이 길어질수록 소프트웨어를 지속적으로 개선해야 한다.

리팩토링을 통해 이해하고 변경하기 쉽게 코드를 개선함으로써 변화하는 요구 사항을 적은 비용으로 반영할 수 있다. 이는 소프트웨어의 생존 시간을 늘려준다.

테스트 대상 코드의 리팩토링 시점

테스트 대상 코드에서 상수를 변수로 바꾸거나 변수 이름을 변경하는 것과 같은 작은 리팩토링은 발견하면 바로 실행한다. 반면에 메서드 추출과 같이 메서드의 구조에 영향을 주는 리팩토링은 큰 틀에서 구현 흐름이 눈에 들어오기 시작한 뒤에 진행한다.

코드 구조가 잘못되면 다음 테스트를 통과시키는 과정에서 코드가 복잡해지거나 구현을 더는 진행하지 못하고 막힐 수 있다. 이런 상황이 오면 구현을 멈추고 메서드 추출 리팩토링을 되돌려야 한다. 리팩토링을 취소해서 코드를 원상 복구한 뒤에 다음 테스트를 진행한다. 그런뒤 코드의 의미나 구조가 더 명확해지면 그때 다시 리팩토링을 시도한다.

테스트 작성 순서 연습

매달 비용을 지불해야 사용할 수 있는 유료 서비스가 있다고 해보자

  • 서비스를 사용하려면 매달 1만원을 선불로 납부한다. 납부일 기준으로 한 달 뒤가 서비스 만료일이 된다.

  • 2개월 이상 요금을 납부할 수 있다.

  • 10만원을 납부하면 서비스를 1년 제공한다.

쉬운 것부터 테스트

이제 테스트 메서드를 추가하자.

  • 우선 가장 쉬어 보이는 만료일을 계산하는 테스트 코드를 작성해 보자

  • 계산에 필요한 값은 납부일과 납부액이고 결과는 계산된 만료일이다.

    @Test
    void 만원_납부하면_한달_뒤가_만료일이_됨() {
        LocalDate billingDate = LocalDate.of(2019,3,1);
    		int payAmount = 10,000;
    
    		ExpiryDateCalculator cal = new ExpiryDateCalculator();
    		LocalDate expiryDate = cal.calculateExpiryDate(billingDate, payAmount);
        
    		assertEquals(LocalDate.of(2019,4,1),expiryDate);
    }
  • 테스트 코드를 성공하기 위해 구현 코드를 작성하자

    public LocalDate calculateExpiryDate(PayData payData) {
        return LocalDate.of(2019,4,1);
    }
  • 테스트 케이스를 추가한 후, 구현 코드를 일반화 하자

    public LocalDate calculateExpiryDate(PayData payData) {
        return payData.plugMonth(1);
    }

코드 정리:중복 제거

  • 테스트 코드의 상수를 변수화한다.

  • 테스트 코드의 중복된 영역을 메소드로 분리 한다.

    @Test
    void 만원_납부하면_한달_뒤가_만료일이_됨() {
        assertExpiryDate(
                PayData.builder()
                        .billingDate(LocalDate.of(2019, 3, 1))
                        .payAmount(10_000)
                        .build(),
                LocalDate.of(2019, 4, 1));
        assertExpiryDate(
                PayData.builder()
                        .billingDate(LocalDate.of(2019, 5, 5))
                        .payAmount(10_000)
                        .build(),
                LocalDate.of(2019, 6, 5));
    }
    
    private void assertExpiryDate(PayData payData, LocalDate expectedExpiryDate) {
        ExpiryDateCalculator cal = new ExpiryDateCalculator();
        LocalDate realExpiryDate = cal.calculateExpiryDate(payData);
        assertEquals(expectedExpiryDate, realExpiryDate);
    }

예외 상황 처리

  • 쉬운 구현을 하나 했으니 이제 예외 상황을 찾아보자

  • 단순히 한 달 추가로 끝나지 않는 상황이 존재한다.

    • 납부일이 2019-01-31이고 납부액이 1만 원이면 만료일은 2019-02-28이다.

    • 납부일이 2019-05-31이고 납부액이 1만 원이면 만료일은 2019-06-30이다.

    • 납부일이 2020-01-31이고 납부액이 1만 원이면 만료일은 2020-02-29이다.

  • 테스트 코드를 작성해보자

    @Test
    void 납부일과_한달_뒤_일자가_같지_않음() {
        assertExpiryDate(
                PayData.builder()
                        .billingDate(LocalDate.of(2019, 1, 31))
                        .payAmount(10_000)
                        .build(),
                LocalDate.of(2019, 2, 28));
        assertExpiryDate(
                PayData.builder()
                        .billingDate(LocalDate.of(2019, 5, 31))
                        .payAmount(10_000)
                        .build(),
                LocalDate.of(2019, 6, 30));
        assertExpiryDate(
                PayData.builder()
                        .billingDate(LocalDate.of(2020, 1, 31))
                        .payAmount(10_000)
                        .build(),
                LocalDate.of(2020, 2, 29));
    }
  • 테스트 코드는 바로 통과한다. 왜냐하면 LocalDate.plugMonths() 메서드가 알아서 한 달 추가 처리를 해주기 때문이다.

다음 테스트 선택: 다시 예외 상황

  • 다음 테스트를 선택하자. 그다음으로 쉽거나 예외적인 것을 선택하면 된다. 다음은 생각할 수 있는 쉬운 예이다.

    • 2만 원을 지불하면 만료일이 두 달 뒤가 된다.

    • 3만 원을 지불하면 만료일이 세 갈 뒤가 된다.

  • 쉬운 것을 테스트할까? 예외 상황을 먼저 테스트할까?

  • 예외 상황을 테스트하려면 첫 납부일이 필요하다. 앞서 작성한 테스트는 납부일과 납부액만 사용했기 때문에 기존 ㅗ드에 첫 납부일을 추가하는 작업이 필요하다.

다음 테스트를 추가하기 전에 리팩토링

  • 만료일을 계산하는데 필요한 값이 세 개로 늘렀다.

    • calculateExpiryDate 메서드의 파라미터로 첫 납부일 추가

    • 첫 납부일, 납부일, 납부액을 담은 객체를 calculateExpiryDate 메서드에 전달

  • 리팩토링을 진행하고 나면 ExpiryDateCalculator 코드는 다음과 같이 변경된다.

    public class ExpiryDateCalculator {
        public LocalDate calculateExpiryDate(PayData payData) {
            ...
        }
    }

예외 상황 테스트 진행 계속

  • 리팩토링을 했으니 다시 테스트를 추가하자

    • 첫 납부일이 2019-01-31이고 만료되는 2019-02-28에 1만 원을 납부하면 다음 만료일은 2019-03-31이다.

    @Test
    void 첫_납부일과_만료일_일자가_다를때_만원_납부() {
        PayData payData = PayData.builder()
                .firstBillingDate(LocalDate.of(2019, 1, 31)) // 납부일 추가
                .billingDate(LocalDate.of(2019, 2, 28))
                .payAmount(10_000)
                .build();
    
        assertExpiryDate(payData, LocalDate.of(2019, 3, 31));
    }
  • 테스트는 실패하였다. 구현 코드를 수정해보자.

    public class ExpiryDateCalculator {
        public LocalDate calculateExpiryDate(PayData payData) {
            if(payDate.getFirstBillingDate().equals(LocalDate.of(2019,1,31))) {
    						return LocalDate.of(2019,3,31);
    				}
    				return payData.getBillingDate().plugMonths(1);
        }
    }
  • 첫 번째 테스트는 통과했지만 두 테스트가 실패했다.

    • getFirstbillingDate가 null 체크를 구현 코드에 추가하였다.

  • 상수를 이용해서 테스트를 통과시켰으니 구현을 일반화할 차례다.

    public class ExpiryDateCalculator {
        public LocalDate calculateExpiryDate(PayData payData) {
            if(payDate.getFirstBillingDate() != null) {
    						LocalDate candidateExp = payDate.getBillingDate().plusMonths(1);
    						if(payData.getFirstBillingDate().getDayOfMonth() != 
    								candidateExp.getDatOhMonth()) {
    								return candidateExp.withDayOfMonth(
    										payData.getFirstBillingDate().getDayOfMonth());
    						}
    				}
    				return payData.getBillingDate().plugMonths(1);
        }
    }
  • 테스트는 통과하였다.

코드 정리: 상수를 변수로

  • plugMonths(1) 를 사용하였다. 1은 만료일을 계산할 때 추가할 개월 수를 의미한다.

  • 상수 1을 변수로 변경하자

다음 테스트 선택: 쉬운 테스트

  • 다음 테스트를 선택하자.

    • 2만 원을 지불하면 만료일이 두 달 뒤가 된다.

    • 3만 원을 지불하면 만료일이 석 달 뒤가 된다.

  • 테스트 코드를 추가해 보자

    @Test
    void 이만원_이상_납부하면_비례해서_만료일_계산() {
        assertExpiryDate(
                PayData.builder()
                        .billingDate(LocalDate.of(2019, 3, 1))
                        .payAmount(20_000)
                        .build(),
                LocalDate.of(2019, 5, 1));
    }
  • 구현 코드를 수정해보자

    public LocalDate calculateExpiryDate(PayData payData) {
    		// addedMonths를 금액에 따라 수정
    		int addedMonths = payData.getPayAmount() / 10_000;
    
        if(payDate.getFirstBillingDate() != null) {
    				LocalDate candidateExp = payDate.getBillingDate().plusMonths(1);
    				if(payData.getFirstBillingDate().getDayOfMonth() != 
    						candidateExp.getDatOhMonth()) {
    						return candidateExp.withDayOfMonth(
    								payData.getFirstBillingDate().getDayOfMonth());
    				}
    		}
    		return payData.getBillingDate().plugMonths(1);
    }

예외 상황 테스트 추가

  • 이번에 추가할 상황은 첫 납부일과 납부일의 일자가 다를 때 2만 원이상 납부한 경우이다.

    • 첫 납부일이 2019-01-31이고 만료되는 2019-02-28에 2만원을 납부하면 다음 만료일은 2019-04-30이다.

  • 테스트 코드를 추가하자

    @Test
    void 첫_납부일과_만료일_일자가_다를때_이만원_이상_납부() {
        assertExpiryDate(
                PayData.builder()
                        .firstBillingDate(LocalDate.of(2019, 1, 31))
                        .billingDate(LocalDate.of(2019, 2, 28))
                        .payAmount(20_000)
                        .build(),
                LocalDate.of(2019, 4, 30));
    }
  • 이는 익셉션이 발생한다. 왜냐하면 4월에는 31일이 없는데 31일로 설정해서 발생한 것임을 알 수 있다.

    public LocalDate calculateExpiryDate(PayData payData) {
    		int addedMonths = payData.getPayAmount() / 10_000;
    
        if(payDate.getFirstBillingDate() != null) {
    				LocalDate candidateExp = payDate.getBillingDate().plusMonths(1);
    				if(payData.getFirstBillingDate().getDayOfMonth() != 
    						candidateExp.getDatOhMonth()) {
    						// 날짜가 잘못 계산되어 익셉션이 발생한다. 
    						return candidateExp.withDayOfMonth(
    								payData.getFirstBillingDate().getDayOfMonth());
    				}
    		}
    		return payData.getBillingDate().plugMonths(1);
    }
  • 이 테스트를 통과시키려면 다음 조건을 확인해야 한다.

    • 후보 만료일이 포함된 달의 마지막 날 < 첫 납부일의 일자

    public LocalDate calculateExpiryDate(PayData payData) {
    		int addedMonths = payData.getPayAmount() / 10_000;
    
        if(payDate.getFirstBillingDate() != null) {
    				LocalDate candidateExp = payDate.getBillingDate().plusMonths(1);
    				if(payData.getFirstBillingDate().getDayOfMonth() != 
    						candidateExp.getDatOhMonth()) {
    						// 후보 만료일이 포함된 달의 마지막 날 < 첫 납부일의 일자 조건 추가
    						if(YearMonth.from(candidateExp).lengthOfMonth() <
    							payData.getFirstBillingDate().getDayOfMonth()) { 
    								return candidateExp.withDayOfMonth(
    										payData.getFirstBillingDate().getDayOfMonth());
    							}
    				}
    		}
    		return payData.getBillingDate().plugMonths(1);
    }

시작이 안 될 때는 단언부터 고민

테스트 코드를 작성하다 보면 시작이 잘 안 될 때가 있다. 이럴 땐 검증하는 코드부터 작성하기 시작하면 도움이 된다. 예를 들어 만료일 계산 기능의 경우 만료일을 검증하는 코드부터 작성해 보는 것이다.

  • 먼저 만료일을 어떻게 표현할지 결정해야 한다.

  • 만료일이므로 날짜를 표현하는 타입을 선택하면 좋을 것 같다.

  • 다음은 실제 만료일을 바꿀 차례다. 이 값은 만료일을 실제로 계산한 결과값을 갖는 변수로 바꿀 수 있다.

  • 어떤 객체의 메서드를 실행해서 계산 기능을 실행하도록 하자

  • 이제 두 가지를 정해야 한다.

    • cal의 타입 : 간단한 만료일 계산을 뜻하는 ExpiryDateCalculator로 정했다.

    • 파라미터 타입 : 만원을 납부했을 때 한 달 뒤가 만료일이 되는지를 테스트할 것이므로 납무일과 납부액을 전달한다.

  • 이렇게 테스트 코드를 어떻게 작성할지 감을 못 잡겠다면 검증 코드부터 시작해보자.

구현이 막히면

TDD를 작성하다 보면 어떻게 해야 할지 생각이 잘 나지 않거나 무언가 잘못한 것 같은 느낌이 들것이다. 이럴 땐 과감하게 코드를 지우고 미련 없이 다시 시작한다. 어떤 순서로 테스트 코드를 작성했는지 돌이켜보고 순서를 바꿔서 다시 진행한다. 다시 진행할때에는 다음을 상기한다.

  • 쉬운 테스트, 예외적인 테스트

  • 완급 조절

PreviousCHAP 02. TDD 시작NextCHAP 04. TDD/기능 명세/설계

Last updated 4 years ago

Was this helpful?