TDD에 대해서
TDD에 대해서
TDD란 무엇인가
- 간단히 말하자면, 테스트 코드를 먼저 짜고 그 이후에 코드를 짜는 방법
- 요구사항 파악
- 테스트 코드 작성
- 테스트 코드가 돌아가도록 코드 작성
- 리팩토링
Why
- 장점
- 동작하는 코드에 대한 자신감 (Clean Code that works)
- 과도한 설계를 피하고, 간결성 증대
- 실행 가능한 문서를 가짐
- 어떻게 그 함수와 코드를 사용해야하는지 명세서가 될 수 있음.
- 디자인적 유연함, 의존성 관리 편함
How
과학과 엔지니어링
도메인
- 소프트웨어는 문제를 푸는 도구
- 도메인은 소프트웨어가 풀어야 할 문제가 정의되는 공간 비즈니스 시스템의 도메인은 비즈니스
- 문제를 충분히 이해하지 못하면 문제를 푸는 도구를 잘 만들 수 없다.
비즈니스 시스템의 도메인 지식 흐름
비즈니스. 분석가 프로그래머 컴퓨터 전문가
<- 목적/추상적 ——————————————– 수단/구체적 ->
- 비즈니스 전문가
- 문제를 가장 잘 이해
- 시스템이 투영해야 할 핵심 지식의 원천
- 문제 설명력 부족
- 풀이도 가장 잘 이해한다고 착각
- 분석가(기획자, QA)
- 비지니스 전문가로부터 시스템 요구사항을 발굴
- 발굴된 요구사항의 오류 탐색
- 발견된 문제점을 구현 작업 전에 협업을 통해 해결
- 프로그래머
- 정제된 기능 명세를 아키텍처와 코드로 번역
- 끊임없는 설계 결정
- 지식 흐름 과정의 마지막 인간
- 컴퓨터
- 코드를 통해 프로그래머로부터 지식을 전달받음
- 철저히 수동적
- 융통성 없음
프로그래머와 기능 명세
- 컴퓨터는 스스로 설계를 결정하지 않기 때문에 프로그래머가 도메인 지식을 컴퓨터에 전달할 때엔 모든 요소들이 명확히 결정될 수 밖에 없음
- 충분히 명확한 도메인 지식을 확보하지 못한 프로그래머는 지식 흐름 상류에 지식 보강을 요청해야함
- 하지만 어떤 프로그래머는 스스로 결정을 내림
- 도메인 지식 투영에 오차 발생
- 무책임하고 위험한 도박!
- 항상 팀장,기획자에게 요청을 하자
테스트 기법
수동 테스트
- 품질 담당자가 UI를 사용해 기능을 검증
- 최종 사용자의 사용 경험과 가장 비슷하게 검증
- 실행 비용이 높고 결과의 변동이 큼
- 가장 온전한 코드 실행
- 인수 테스트
소프트웨어 회귀(software regression)
원래 동작하던 코드가 어느 시점 부터 동작하지 않는 현상
테스트 자동화
- 기능을 검증하는 코드를 작성
- 테스트 코드 작성 비용이 소비되지만 실행 비용이 낮고 결과의 신뢰도가 높음
- 테스트 코드 작성과 관리가 프로그래머 역량에 크게 영향 받음
인수 테스트
- 배치된 시스템을 대상으로 검증
- 전체 시스템 이상 여부 신뢰도가 높음
- 높은 비용
- 피드백 품질이 낮음
단위 테스트
- 시스템의 일부(하위 시스템)을 대상으로 검증
- 낮은 비용 (작성비용/관리비용/실행비용)
- 높은 피드백 품질
- 전체 시스템 이상 여부 신뢰도가 낮음
코드 분해
문제의 크기
- 프로그래머가 한 번에 다룰 수 있는 문제의 크기는 한계를 가짐
- 프로그래머는 더 큰 문제를 자주 마주함
- 시스템의 크기는 점점 커짐
- 큰 문제는 작은 문제로 분해할 수 있음
- 작은 문제의 일부는 반복됨
한번 작성한 코드는 계속해서 읽히게 된다. -코드 재사용-
- 새로운 코드를 작성할 때 기존의 코드를 읽게 된다.
- 기존 코드를 수정할 때도 읽게 된다.
- 반복되는 문제의 풀이는 기존 코드를 재사용해서 해결 가능하다.
- 결국 가독성이 좋은 코드가 소프트웨어 개발 비용을 절감한다.
코드를 재사용할 때, 가장 먼저 고려해야 할 것은 그대로 다시 사용하는 것
- 코드는 다른 코드와 연결되어 있기 때문에, 수정할 때는 조심해야 한다.
모듈화
- 분해
- 큰 시스템은 더 작은 하위 시스템으로 분해 가능
- 교체 가능
- 조립
- 작은 시스템은 더 큰 상위 시스템으로 조립 가능
- 모듈 재사용
- 라이브러리
- 단위 테스트
구현, 인터페이스
- 인터페이스
- 모듈이 어떤 기능을 제공하는지, 그 기능을 제공받기 위해서 어떻게 사용해야 하는지 알 수 있다.
- 구현
- 어떻게 만들어지는지에 대해 알 수 있다.
모듈을 모아 상위 시스템을 만들 때는, 인터페이스를 고려해서 새로운 시스템을 설계 해야한다.
- 따라서 구현과 인터페이스가 잘 구분되어 있어야 한다.
- 구현과 인터페이스가 잘 구분되어 있으면, 가독성을 높이고 실수를 줄인다.
단위 테스트
예시)
// 운영코드
function refineText(s) {
return s.replace(" ", " ").replace(" ", " ");
}
module.exports = refineText;
- jest –watch 를 사용해서 test 코드를 작성
const sut = require("./test");
test('sut transforms "hello world" to "hello world"', () => {
const actual = sut("hello wolrd");
expect(actual).toBe("hello wolrd");
});
jest parameterize test
- 테스트 코드를 실행 할 때, 동일한 테스트 코드를 여러개의 테스트 데이터를 넣어주면서 테스트 해주는 기능
const sut = require("./test");
test.each`
source | expected
${"hello world"} | ${"hello world"}
${"hello world"} | ${"hello world"}
${"hello world"} | ${"hello world"}
`('sut transforms "$source" to "$expected"', ({ source, expected }) => {
const actual = sut(source);
expect(actual).toBe(expected);
});
테스트 우선 개발
- 테스트 코드를 운영 코드보다 먼저 개발하는 방법 입니다.
테스트 코드
- 가시적이고 구체적인 목표 - 기능명세 역할을 함
- 자가검증
- 반복실행 - 언제든지 사용 가능
- 클라이언트 - 운영코드 API의 클라이언트가 됨
운영 코드보다 테스트 코드를 먼저 작성
- 명확하고 검증 가능한 목표를 설정한 후 목표를 달성
- 프로세스가 코딩에 앞선 목표 설정을 강요
- 프로그래머는 자신이 풀어야 할 문제를 구체적으로 이해해야 함
기존에 있던 코드로 다시 돌아갑시다.
기존의 운영 코드를 테스트가 통과하도록 수정해 봅시다
-
기존 운영 코드
// prod 함수 function refineText(s) { return s.replace(" ", " ").replace(" ", " "); } module.exports = refineText; -
수정 운영 코드
// prod 함수 function refineText(s) { return s.replace(" ", " ").replace(" ", " ").replace(" ", " "); } module.exports = refineText;
어처구니 없이 replace 를 추가해주니 모든 테스트를 성공했습니다.
-
테스트 케이스를 추가해 보았습니다.
const sut = require("./test"); test.each` source | expected ${"hello world"} | ${"hello world"} ${"hello world"} | ${"hello world"} ${"hello world"} | ${"hello world"} ${"hello world"} | ${"hello world"} ${"hello world"} | ${"hello world"} ${"hello world"} | ${"hello world"} `('sut transforms "$source" to "$expected"', ({ source, expected }) => { const actual = sut(source); expect(actual).toBe(expected); });마지막 케이스에서 실패 했습니다.
탭 문자가 포함 되어 있을때 케이스를 개발하기
-
우선 테스트에 tab케이스가 포함된 경우를 작성해줍니다.
const sut = require("./test"); test.each` source | expected ${"hello world"} | ${"hello world"} ${"hello world"} | ${"hello world"} ${"hello world"} | ${"hello world"} ${"hello world"} | ${"hello world"} ${"hello world"} | ${"hello world"} ${"hello world"} | ${"hello world"} `('sut transforms "$source" to "$expected"', ({ source, expected }) => { const actual = sut(source); expect(actual).toBe(expected); }); test.each` source | expected ${"hello\t world"} | ${"hello world"} `( 'sut transforms "$source" that contains tab character to "$expected"', ({ source, expected }) => { const actual = sut(source); expect(actual).toBe(expected); } ); -
그리고 운영코드를 수정합니다.
// prod 함수 function refineText(s) { return s .replace(" ", " ") .replace(" ", " ") .replace(" ", " ") .replace(" ", " ") .replace("\t ", " "); } module.exports = refineText;이러한 과정을 반복하며 개발합니다.
- 테스트 케이스 추가 -> 운영 코드 수정 -> 테스트 케이스 추가 -> 운영 코드 수정
새로운 기능 추가
특정 단어가 오면 마스킹 처리를 하는 기능을 개발 해봅시다.
-
테스트 코드 개발
test.each` source | bannedWords | expected ${"hello mockist"} | ${["mockist", "purist"]} | ${"hello *******"} `( 'sut transforms "$source" to "$expected"', ({ source, bannedWords, expected }) => { const actual = sut(source, { bannedWords }); expect(actual).toBe(expected); } ); -
운영 코드 수정
// prod 함수 function refineText(s) { return s .replace(" ", " ") .replace("\t", " ") .replace(" ", " ") .replace(" ", " ") .replace(" ", " ") .replace("\t ", " ") .replace("mockist", "*******"); } module.exports = refineText;
언제까지 이럴게 할 것인가…
random test를 해봅시다
-
test code
describe("given banned word", () => { const bannedWord = faker.lorem.word(); const source = "hello " + bannedWord; const expected = "hello " + "*".repeat(bannedWord.length); test("${bannedWord} when invoke sut then it returs ${expected", () => { const actual = sut(source, { bannedWords: [bannedWord] }); expect(actual).toBe(expected); }); }); -
상용 code
// prod 함수function refineText(s, options) { s = s .replace(" ", " ") .replace("\t", " ") .replace(" ", " ") .replace(" ", " ") .replace(" ", " ") .replace("\t ", " ") .replace("mockist", "*******") .replace("purist", "******"); if (options) { for (const bannedWord of options.bannedWords) { s = s.replace(bannedWord, "*".repeat(bannedWord.length)); } } return s;}module.exports = refineText;
리팩토링
리팩토링은 작업 환경 정리다
- 생상선 : 정리된 환경과 어지럽혀진 확녕에서의 작업 생산성 차이
- 지속성 : 작업 환경의 생산성이 일정 수준 미만으로 떨어지면 더 이상 그 환경에서 작업 진행은 불가능
- 코드는 작업 환경이자 작업 결과물
Re-factoring
- 리팩토링은 의미를 유지하며 코드베이스를 정리하는 것
- 하지만 많은 개발자들이 의미가 변하지 않는 것을 보장하는 작업을 하지 않는다.
- 이 보장하는 작업은 테스트이다.
테스트 주도 개발

- RED
- 테스트 우선개발
- 운영코드를 작성하기 전에 테스트를 추가하는 과정
- GREEN
- 운영 코드를 수정해서 테스트를 통과 시키는 과정
- REFACTOR
- 테스트를 통과하면서 작성한 코드를 리팩토링하는 과정
테스트 실패
- 구체적인 하나의 요구사항을 검증하는 하나의 테스트를 추가
- 추가된 테스트가 실패하는지 확인
테스트 성공
- 추가된 테스트를 비롯해 모든 테스트가 성공하도록 운영 코드를 변경
- 테스트 성공은 요구사항 만족을 의미 - 코딩의 가장 중요한 임무
- 테스트 성공을 위한 최소한의 변경
리팩토링
- 코드베이스 정리
- 구현 설계 개선
- 가독성
- 적응성
- 성능
- 모든 테스트 성공을 전제
켄트 벡의 설계 규칙
- Passes the tests : 빠르게 테스트 성공을 해야함
- Reveals intention : 의도를 노출함, 가독성을 높임
- No duplication : 중복 제거
- Fewest elements : 테스트 성공에 필요하지 않은 코드를 제거해라
테스트 주도 개발 세부 흐름
- 단위 테스트 작성
- 단위 테스트 실행
- (실패하면) 운영 코드 작성
- 단위테스트 실행
- 설계 계선
- 단위테스트 실행
테스트 주도 개발은 낯설지 않다.
- 버그 리포트 -> 버그 재현 -> 오류 발생 -> 코드 수정 -> 버그 재현
프로그래머 피드백
기대 출력 피드백
- 사용자 피드백
- 사용자가 직접 코드를 사용한 후 경험한 버그나 불만을 제보
- Quality Assurance(QA)
- 전문 인적 자원에 의한 인수 테스트
- 프로그래머 테스트
- 프로그래머가 직접 피드백 장치를 준비
- 도구 피드백
- 컴파일 오류, 정적 검사 등 프로그래머가 사용하는 도구가 제공하는 피드백
오버 엔지니어링
- 프로그래머는 요구사항 명세에 명확히 지정되지 않은 성능 달성이나 구현 설계 품질 개선에 빠져드는 경향을 가짐
- 이런 목표는 그 자체로 나쁜 것이 아니지만 지나치면 더 중요한 목적, 기능 요구사항에 써야 할 자원을 불필요하게 낭비하게 됨
- 테스트 주도 개발은 가장 중요한 목표를 우선 달성하도록 유도하며 오버엔지니어링에 빠졌음을 느낄 때 안심하고 다음으로 나아갈 수 있도록 피드백을 제공
핵심은 피드백
- 테스트 주도 개발의 핵심은 정해진 절차가 아니라 짧은 주기로 지속되는 피드백
- 피드백에 기반해 안정적으로 지식과 코드를 늘려 나가는 것이 목적
연습하기 1
게임 설계
- 1부터 100까지의 임의의 정수를 맞추는 게임
- 플레이어가 숫자를 입력하면
- 입력한 숫자가 정답보다 작으면 작다고 출력
- 입력한 숫자가 정답보다 크면 크다고 출력
- 입력한 숫자가 정답보다 일치하면 라운드 종료
- 단일 플레이어 모드와 다중 플레이어 모드 지원
- 단일 플레이어 모드 라운드가 종료되면 총 시도를 출력
- 다중 플레이어모드 라운드가 종료되면 승자를 출력
Comments