4. 테스트 구축하기
리팩터링을 제대로 하려면 불가피하게 저지르는 실수를 잡아주는 견고한
테스트스위트
가 뒷밤침 되어야 한다.
- 리팩터링을 하지 않더라도 좋은 테스트를 작성하는 일은 개발효율을 높혀준다.
4.1 자가 테스트 코드의 가치
- 개발할때 실제로 코드를 작성하는 시간의 비중은 크지 않다. (현행파악, 설계고민, 디버깅 등)
- 모든 테스트를 완전히 자동화하고 그 결과까지 스스로 검사하게 만들자
- 컴파일때마다 테스트도 함께 실행 > 생산성 증가
- 테스트를 자주 수행하는 습관은 버그를 찾는 강력한 도구가 될 수 있다.
테스트 스위트(test suite)
는 강력한 버그 검출 도구로, 버그를 찾는 데 걸리는 시간을 대폭 줄여준다.Junit
: 스몰토크 버전의 단위 테스트 프레임워크를 켄트백이 Java로 포팅한 것
- 테스트를 작성하기 가장 좋은 시점은 프로그래밍을 시작하기 전이다.
- 기능을 추가할때 테스트 부터 작성한다.
- 그럼 원하는 기능을 추가하기 위해 무엇이 필요한지 인터페이스에 집중하게 될 수 있다.
- 테스트를 모두 통과한 시점이 코드를 완성한 시점
테스트 주도 개발(Test-Driven Development)
: 테스트 - 코딩 - 리팩터링을 짧은 주기로 반복- 테스트가 갖춰지지 않은 코드를 리팩토링해야할 경우는 먼저 자가 테스트 코드부터 작성
4.2 테스트할 샘플 코드
- 생산 계획은 각 지역(province)의 수요(demand)와 가격(price)으로 결정
- UI,영속성,외부서비스와 관련 없는 가장 쉬운 코드부터 테스트 작성
- 코드는 항상
성격에 따라 분리
하는 것이 좋다. - 비즈니스 로직 코드
- 지역(Province) : Json 문서로부터 만들어진 자바스크립트 객체를 생성자로 받아 생성
- 생산자(Producer) : 단순 데이터 저장용
class Province {
constructor(doc) {
this._name = doc.name;
this._producers = [];
this._totalProduction = 0;
this._demand = doc.demand;
this._price = doc.price;
doc.producers.forEach(d => this.addProducer(new Producer(this, d)));
}
get name() {
return this._name;
}
get producers() {
return this._producers.slice();
}
get totalProduction() {
return this._totalProduction;
}
set totalProduction(arg) {
this._totalProduction = arg;
}
get demand() {
return this._demand;
}
set demand(arg) {
this._demand = parseInt(arg);
} // 숫자로 파싱해서 저장
get price() {
return this._price;
}
set price(arg) {
this._price = parseInt(arg);
} // 숫자로 파싱해서 저장
addProducer(arg) {
this._producers.push(arg);
this._totalProduction += arg.production;
}
get shortfall() {
return this._demand - this.totalProduction;
}
get profit() {
return this.demandValue - this.demandCost;
}
get demandValue() {
return this.satisfiedDemand * this.price;
}
get satisfiedDemand() {
return Math.min(this._demand, this.totalProduction);
}
get demandCost() {
let remainingDemand = this.demand;
let result = 0;
this.producers
.sort((a, b) => a.cost - b.cost)
.forEach(p => {
const contribution = Math.min(remainingDemand, p.production);
remainingDemand -= contribution;
result += contribution * p.cost;
});
return result;
}
}
/* Province 생성자에 필요한 json doc */
function sampleProvinceData() {
return {
name: 'Asia',
producers: [
{name: 'Byzantium', cost: 10, production: 9},
{name: 'Attalia', cost: 12, production: 10},
{name: 'Sinope', cost: 10, production: 6},
],
demand: 30,
price: 20,
};
}
class Producer {
constructor(aProvince, data) {
this._province = aProvince;
this._cost = data.cost;
this._name = data.name;
this._production = data.production || 0;
}
get name() {
return this._name;
}
get cost() {
return this._cost;
}
set cost(arg) {
this._cost = parseInt(arg);
}
get production() {
return this._production;
}
set production(amountStr) {
const amount = parseInt(amountStr);
const newProduction = Number.isNaN(amount) ? 0 : amount;
this._province.totalProduction += newProduction - this._production;
this._production = newProduction;
}
}
4.3 첫 번째 테스트
- 테스트 프레임워크는 본책에서는
모카(Mocha)
를 사용하나 본 포스팅에서는Jest
를 사용했습니다.
생산부족분 테스트
- 테스트에 필요한 데이터와 객체를 뜻하는
픽스처(fixture)
를 설정 - 픽스처의 속성 검증
describe('province', function () {
it('shortfall', function () {
const asia = new Province(sampleProvinceData()); // 1.픽스처 설정
expect(asia.shortfall).toBe(5); // 2.검증
});
});
실패해야 할 상황에는 반드시 실패하게 만들자
- 테스트가 실패하는 모습을 최소한 한 번씩은 직접 확인해 봐야한다.
- 일시적으로 코드에 오류를 주입하여 테스트 실패를 확인해본다.
get shortfall() {
return this._demand - this.totalProduction * 2; // 오류 주입
}
/*
Error: expect(received).toBe(expected) // Object.is equality
Expected: 5
Received: -20
*/
자주 테스트하라. 작성중인 코드는 최소한 몇 분 가격으로 테스트하고, 적어도 하루에 한 번은 전체 테스트를 돌려보자
- 테스트를 자주 수행하며 리팩터링하거나 추가한 코드에 문제가 없는지 확인하자.• 단체견적 옷 퀄리티가 가격대비 좋지 않음
- 테스트 진행 상태를
초록막대(green bar)
,빨간막대(redbar)
로 이야기함. - 빨간막대일때는 리팩터링 하지말라!
초록막대
로 다시 돌아가라
4.4 테스트 추가하기
- 테스트는
위험요인을 중심
으로 작성 되어야 한다. - 테스트의 목적은 현재 혹은
향후에 발생하는 버그를 찾는데 있다.
- 테스트를 너무 많이 만들다 보면 오히려 필요한 테스트를 놓치기가 쉽다.
- 단순 접근자 같은 코드는 테스트 할 필요가 없다.
완벽하게 만드느라 테스트를 수행하지 못하느니, 불완전한 테스트라도 작성해 실행하는게 낫다.
기대값 채우기
- 기대값 230은 임의의 값을 넣고 테스트 후 실제 계산되는 값을 대입함
임시값 설정
>실제값 대체
>오류심기
>되돌리기
it('profit', function () { // 총 수익 테스트
const asia = new Province(sampleProvinceData());
expect(asia.profit).toBe(230);
});
중복 제거
- 테스트 코드에서도
중복은 의심
해야 한다. - 테스트끼리 상호작용하게 하는 공유 픽스처는 지양해야 한다.
beforeEach
구문을 사용해 테스트 바로전에 asia를 초기화 하여 모든 테스트가 각각의 asia 인스턴스를 사용- 매번 생성한다고 테스트가 느려지는 경우는 거의 없다.
- 예외적으로 불변임이 확실한 픽스처는 공유하기도 한다.
- 공유픽스처를 사용하면 디버깅하는데 고생할 수 있다.
describe('province', function () {
// const asia = new Province(sampleProvinceData()); // 테스트끼리 상호작용하게 하는 공유 픽스처 사용 X
let asia;
beforeEach(function(){ // 각각의 테스트마다 픽스처 초기화
asia = new Province(sampleProvinceData());
})
it('shortfall', function () { // 생산부족분 테스트
expect(asia.shortfall).toBe(5); // 2.검증
});
it('profit', function () { // 총 수익 테스트
expect(asia.profit).toBe(230);
});
});
4.5 픽스처 수정하기
- 실전에서는
사용자가 값을 변경하면서 픽스처의 내용도 수정되는 경우
가 흔하다. - 이런 수정은 대부분
setter
에서 이루어져서 잘 테스트하지 않지만 이번 예제의Producer
>production()
setter가 복잡하니 테스트를 수행할 필요가 있다.
it('change production', function () { // production 변경 테스트
asia.producers[0].production = 20;
expect(asia.shortfall).toBe(-6);
expect(asia.profit).toBe(292);
});
- beforeEach 블록에서
설정
한 표준픽스처를 취하고 - 테스트를
수행
후 - 픽스처가 일을 기대한대로 처리했는지
검증
설정(setup)-실행(exercise)-검증(verify)
조건(given)-발생(when)-결과(then)
준비(arrange)-수행(act)-단언(assert)
- 이 세 가지 단계가 한 테스트안에 모두 담겨 있을 수도 있고 초기 준비작업중 공통 부분을 beforeEach 같은 표준 설정 루틴에 모아서 처리하기도 한다.
해제(teardown)
혹은청소(cleanup)
라는 네번째 단계에서 픽스처를 제거하여 테스트들이 서로 영향을 주지 못하게 막는 단계를 두기도 함. (beforeEach 를 사용했기에 생략)
4.6 경계 조건 검사하기
꽃길(Happypath)
상황 외에 범위를 벗어나는 경계 지점에서 문제가 생기면 어떤일이 벌어지는지 확인하는 테스트도 작성하면 좋다.
컬렉션이 있을 경우 컬렉션이 비었을때 발생
하는 테스트를 확인
describe('no producers', function () { // 생산자가 없다.
let noProducersProvince;
beforeEach(function(){
const data = {
name: "No producers",
producers: [],
demand: 30,
price: 20
};
noProducersProvince = new Province(data)
})
it('shortfall', function () { // 생산부족분 테스트
expect(noProducersProvince.shortfall).toBe(30);
});
it('profit', function () { // 총 수익 테스트
expect(noProducersProvince.profit).toBe(0);
});
});
숫자형이라면 0, 음수
확인
it('zero demand', function () { // 수요(demand)가 없다.
asia.demand = 0
expect(asia.shortfall).toBe(-25);
expect(asia.profit).toBe(0);
});
it('negative demand', function () { // 수요(demand) 마이너스
asia.demand = -1
expect(asia.shortfall).toBe(-26);
expect(asia.profit).toBe(-10);
});
- 수요가 음수일때 수익이 음수인게 말이 되나?
- 경계를 확인하는 테스트를 작성할때
특이 상황을 어떻게 처리할지 생각
해본다.
수요 입력란이 비어있는 경우 - Null
it('empty string demand', function () { // 수요(demand) 입력란이 비어있다.
asia.demand = "";
expect(asia.shortfall).NaN;
expect(asia.profit).NaN;
});
- 의식적으로 프로그램을 망가뜨리는 방법을 모색한다.
생산자 수 필드에 문자열 대입
describe('string for producers', function () { // 생산자 수 필드에 문자열 대입
let prov;
beforeEach(function(){
const data = {
name: "String producers",
producers: "",
demand: 30,
price: 20
};
prov = new Province(data)
})
it('', function () { // 생산부족분 테스트
expect(prov.shortfall).equal(0);
});
});
/*
doc.producers.forEach is not a function
TypeError: doc.producers.forEach is not a function
*/
실패(Fail) :
실제 값이 예상 범위를 벗어났다는 의미에러(Error):
프로그램 예외 상황 발생- 오류 메시지 출력, 빈배열 설정 등 후처리
- 혹은 그대로 두고 입력 객체를 신뢰할 수 있는 곳에서 만들어 주는 코드를 둔다.
- 같은 코드 베이스 안에서 유효성검사 코드가 너무 많으면 중복으로 검증하여 오히려 문제될 수 있음
- 리팩터링 전이라면 이런 테스트 코드는 작성하지 않음
리팩터링은 겉보기 동작에 영향을 주지 않아야 하기 때문
이다.
어차피 모든 버그를 잡아낼 수 없다고 생각하여 테스트를 작성하지 않는다면 대다수의 버그를 잡을 수 있는 기회를 날리는 셈이다.
테스트는 프로그래밍 속도를 높여 준다.
- 테스트에는 다양한 기법이 있지만 너무 빠져들 필요는 없다. 너무 많이 작성하면 의욕이 떨어져 나중에 하나도 작성하지 않을 수 있다.
- 따라서
테스트는 위험한 부분에 집중하여 작성
하는 것이 좋다. - 테스트는 모든 버그를 걸러주지는 못해도
안심하고 리팩터링 할 수 있는 보호막
이 되어준다. - 리팩터링 전에 테스트 스위트부터 갖추고 리팩터링하는 동안에도 계속 테스트를 추가한다.
4.7 끝나지 않은 여정
단위테스트(Unit Test)
: 코드의 작은 영역만 대상으로 빠르게 실행되도록 설계된 테스트- 단위테스트는 자가 테스트코드의 핵심이며 대부분 테스트 시스템은 단위테스트가 차지한다.
- 프로그래밍하면서 테스트도 반복적으로 진행하며
지속적으로 테스트 스위트를 보강
해 나가야 한다. - 기존 테스트가 명확한지
테스트 과정을 리팩터링
할 수 없는지도 검토해본다.
버그 리포트를 받으면 가장 먼저 그 버그를 드러내는 단위테스트부터 작성하자
- 버그를 발견하는 즉시
버그를 명확하게 잡아낼 수 있는 테스트부터 작성하는 습관
을 들이자 - 테스트 스위트가 충분한지를 평가하는 기준은 주관적이다.
- 테스트를 너무 많이 작성할 경우 수정시 제품코드보다 테스트 코드 수정이 더 걸릴 수 있다.
- 하지만 보통 테스트코드가 더 적은 경우가 많다.
예제 코드 GitHub
'Program > Refactoring' 카테고리의 다른 글
[Refactoring] 5. 리팩터링 카탈로그 보는 법 (1) | 2022.10.23 |
---|---|
[Refactoring] 3. 코드에서 나는 악취 (0) | 2022.09.07 |
[Refactoring] 2. 리팩터링 원칙 (0) | 2022.08.18 |
[Refactoring] 1. 리팩터링: 첫 번째 예시 (0) | 2022.08.18 |