리팩터링을 제대로 하려면 불가피하게 저지르는 실수를 잡아주는 견고한 테스트 스위트(test suite)가 뒷받침 되어야한다.
리팩터링을 위해 하지않더라도 좋은 테스트를 작성하는 일은 개발 효율을 높여준다.
4.1 자가 테스트코드의 가치
디버깅의 시간을 줄이면 생산성이 급 상승한다.
- 실제 코드를 작성하는 시간의 비중보다 디버깅 하는데에 시간을 더 오래 쓴 경험이 누구라도 있다.
- 저자의 테스트 코드의 깨달음을 얻은 실전 후기
- 코드 반복 개발 주기가 끝나면 테스트코드를 추가함
- 테스트 코드가 콘솔에 출력되면 그 값이 일치하는지 눈으로 일일히 확인하여 통과 여부 판단
- 직접 판단하는게 귀찮아서 컴퓨터가 판단하도록 수정하여 모든 테스트가 통과하면 OK 가 보이도록 수정
- 편해져서 컴파일 할때마다 테스트도 함께함.
- 생산성이 급상승했다 > 디버깅시간이 크게 줄었기 때문
- 저자의 테스트 코드의 깨달음을 얻은 실전 후기
- 테스트를 자주하는 습관이 곧 버그를 찾는 강력한 도구가 된다.
- 직전까지 테스트가 성공했는데 이번에 테스트가 실패했다면, 최근 작성한 코드에서 버그가 발생했음을 알 수 있다.
- 코드 작성 후 테스트를 한다면 의심되는 코드양이 많지 않고 기억이 생생하니 버그를 쉽게 찾을 수 있다.
- 함수 몇 개만 작성해도 테스트를 곧바로 추가 하자.
- **회귀 버그(regression bug)**를 잡는데 몇 분 이상 걸린적이 없게 된다.
- 회귀 버그란 잘 작동하던 기능에서 문제가 생기는 현상. 일반적으로 프로그램 변경하는 중 뜻하지않게 발생한다. 같은 맥락에서 잘 작동하던 기능이 여전히 잘 작동하는지 확인하는 테스트를 회귀 테스트라 한다.
어차피 모든 버그를 잡아낼 수 없다고 생각하여 테스트를 작성하지 않는다면 대다수의 버그를 잡을 수 있는 기회를 날리는 셈이다. >
TDD: Test-Driven Development - 테스트 주도 개발
- 테스트 - 코딩 - 리팩터링
- 테스트를 추가하기 가장 좋은 시점은 프로그래밍 전이다.
- 테스트를 작성하다보면 원하는 기능을 추가하기 위해 무엇이 필요한지 고민하게된다.
- 구현보다 인터페이스에 집중하게 된다는 장점도 있다.
- 코딩이 완료되는 시점을 정확하게 판단할 수 있다.
- 테스트를 모두 통과한 시점이 바로 코드를 완성한 시점이다.
테스트가 갖춰지지않은 코드를 리팩터링 해야할 때,
곧바로 리팩토링을 하지않고 자가 테스트 코드부터 작성하라.
- 리팩터링에는 반드시 테스트가 필요하다.
4.2~4.6 테스트 코드 생성부터 수정까지
- 예제 모카 라이브러리
테스트 기본 구조
describe("지역전체 클래스 검사", () => {
it("부족분", () => {
const asia = new Province(sampleProvinceData()); // step 1
assert.equal(asia.shortfall, 5); // step 2
});
});
- 블록단위로 나눠서 각 블록에 테스트 스위트를 담는다.
- describe 블록과 it 블록에는 부연 설명용 문자열을 써넣는다.
- 테스트가 무엇을 검사하는지 설명하거나 명확한 코드에는 주석이 필요없듯 그냥 비워두기도함.
- step1: 테스트에 필요한 데이터와 객체를 뜻하는 픽스처(fixture)를 설정한다.
- step2: 이 픽스처의 속성들을 검증한다.(chai 라이브러리에 존재하는 assert 함수를 이용하여 검증)
- 결과확인: 주어진 초기값에 기초하여 지역전체 클래스에서 생산부족분이 5이면 테스트 성공 아니면 실패
테스트 코드 작성 팁
-
실패해야할 상황에서는 실패하는지도 확인한다.
- 일시적으로 코드에 오류를 주입하여 실패하는지도 테스트하여 테스트 코드가 의도대로 동작하는지 확인한다.
-
자주 테스트하라.
-
명심하자. 테스트는 위험 요인을 중심으로 작성하자!!!
-
적은 수의 테스트만으로 큰 효과를 얻자!
-
테스트코드에서도 중복은 의심하자.
-
테스트끼리 상호작용하게 하는 공유 픽스처는 생성하지말자.
// 💩 CASE describe(description, () => { const asia = new Province(sampleProvinceData()); //💩 // 참조가 바뀔 경우 다른 테스트가 실패할 가능성이 있다!! it('테케1', () => { ... }); it('테케2', () => { ... }); }); // 👍🏻 CASE describe(description, () => { /* 픽스처(초기 준비 작업 중 공통되는 부분) 작성 */ beforeEach(() => { ... }); it('테케1', () => { ... }); it('테케2', () => { ... }); });
- beforeEach 구문은 각각의 테스트 바로전에 실행되어 asia를 초기화하기 때문에 모든 테스트가 자신만의 새로운 asia를 사용하게 된다.
- beforeEach블록: 표준 픽스처를 사용한다는 사실을 알려준다.
- it 구문 하나당 검증도 하나씩 하는 것이 좋다.
-
it 구문은 하나당 검증도 하나씩 하는 것이 좋다. 하지만 밀접하다고 생각되면 묶어서 작성해도된다.
- 복잡한
setter
를 테스트해 보는 caseit("change production", () => { asia.producers[0].production = 20; assert.equal(asia.profit, 292); assert.equal(asia.shortfall, -6); });
- 복잡한
-
문제가 생길 가능성이 있는 경계조건을 생각해보고 경계 조건도 반드시 검사해본다.
- 빈 값이나 음수와 같은 경계조건
describe("no producers", () => { let noProducers; beforeEach(() => { const data = { name: "no producers", producers: [], demand: 30, price: 20, }; noProducers = new Province(data); }); it("shortfall", () => { expect(noProducers.shortfall).equal(30); }); it("profit", () => { expect(noProducers.profit).equal(0); }); }); it("negative demand", () => { asia.demand = -1; expect(asia.shortfall).equal(-26); expect(asia.profit).equal(-10); });
- 입력칸이 비어있을때도 잘 처리되는지 확인해본다.
it("empty string demand", () => { asia.demand = ""; expect(asia.shortfall).NaN; expect(asia.profit).NaN; });
- 타입이 다른 경우도 테스트해본다(배열이 들어와야하는데 문자열넣었을때)
describe("string for producers", () => { it("", () => { const data = { name: "String producers", producers: "", demand: 30, price: 20, }; const prov = new Province(data); expect(prov.shortfall).equal(0); }); });
- 이런 경우 예외처리나 로그를 추가하는 코드를 수정하거나, 에러를 그대로 놔둬도 된다.
- 유효성검사가 너무많으면 중복검사이니 유의해야한다.
- 리팩터링할때에는 겉보기 동작에 영향을 주지 않아야하므로 경계조건에 대응하는 동작이 리팩터링때문에 변하는지는 신경쓰지말자.
-
외부에서 JSON 으로 들어온 객체는 유효한지 확인해야하므로 항상 테스트한다.
-
객체가 유효하다
는 말의 뜻?- 합의된 인터페이스대로 왔는가?
- 합의:
{id: number; value?: string}
- 실제로 넘어온 값:
{id: 1, value: ‘haha’, title: null}
- ⇒ 이러면 합의된 인터페이스가 깨진 것이므로 유효하지 않다!
테스트 코드는 어느 수준까지 작성해야 할까?
- 아무리 테스트코드를 작성을해도 버그없는 완벽한 프로그램을 만들수없다.
- 수확체감법칙
- 너무많이 작성하다보면 의욕이 떨어진다.
- 위험한 부분에 집중하자
- 처리 과정이 복잡한 부분을 찾자
4.7 끝나지 않은 여정
모든 버그를 걸러주지는 못할지라도, 안심하고 리팩터링할 수 있는 보호막은 되어준다.
리팩터링하는 동안에도 계속 테스트를 추가하자.
테스트 용이성을 아키텍처 평가 기준으로 활용하는 사례도 많다.
다양한 유형의 테스트
- 단위테스트
코드의 작은 영역만을 대상으로 빠르게 실행되도록 설계된 테스트, 자가 테스트 코드의 핵심이자, 자가테스트 시스템의 대부분이다. - 컴포넌트 사이의 상호작용에 집중하는 테스트
- 소프트웨어의 다양한 계층의 연동을 검사하는 테스트
- 성능 테스트
테스트의 품질 향상 및 평가 기준
- 한 번에 완벽한 테스트를 갖추기는 어려우므로 테스트 스위트를 지속해서 보강한다. 기존 테스트가 명확한지, 이해하기 쉽게 리팩터링할 수 없는지, 제대로 검사하는지 등을 확인한다.
- 버그리포트를 받으면 가장 먼저 그 버그를 드러내는 단위 테스트부터 작성하는 습관을 들이자.
- 테스트 커버리지 분석은 코드에서 테스트하지 않는 영역을 찾는 데만 도움될 뿐, 테스트 스위트의 품질과는 크게 상관없다.
- 테스트가 충분한지 평가하는 기준은 주관적이다.
- 테스트를 너무 많이 작성할 가능성이 있다. 개발 속도가 느려진다고 생각이 되면 테스트를 과하게 작성하는 것이 아닌지 의심하자.