리팩토링2 스터디 - 004
4장 테스트 구축하기
도입부
- 리팩토링을 제대로 하려면 불가피하게 저지르는 실수를 잡아주는 견고한 테스트 스위트(test suite)가 뒷받침돼야 한다
자가 테스트 코드의 가치
- 테스트를 작성하고 결과를 일일이 확인하지말고 결과값이 예상값과 같으면 화면에 “OK”만 출력되도록 하자
- 모든 테스트를 완전히 자동화하고 그 결과까지 스스로 검사하게 만들자
- 컴파일을 하면 자동으로 테스트를 진행하도록 하면 생산성이 증대한다
- 테스트 케이스를 작성하면 버그도 눈에 띄게 줄게 된다
- 짧은 기간에 반복적인 테스트 작성은 회귀 버그를 잡는데 효과적이다
- 회귀 버그(Regression Bug)
- 잘 작동하던 기능에서 문제가 생기는 현상을 가르키며, 일반적으로 프로그램을 변경하는 중 뜻하지 않게 발생
- 같은 맥락에서 가능이 여전히 잘 작동하는지 확인하는 테스트를 회귀 테스트(Regression Test)라 한다.
- 테스트 스위트는 강력한 버그 검출 도구로, 버그를 찾는 데 걸리는 시간을 대폭 줄여준다
- 1997년 켄트 백, 에릭 감마는 스몰토크 버전 단위 테스트 프레임워크를 자바로 포팅했는데 이것이 바로 JUnit이다.
- 테스트 코드를 작성하기 가장 좋은 시점은 프로그래밍을 시작하기 전이다
- 테스트를 작성하다 보면 원하는 기능을 추가하기 위해 무엇이 필요한지 고민하게 된다
- 구현보다 인터페이스에 집중하게 된다는 장점도 있다
- 게다가 코딩이 완료되는 시점을 정확하게 판단 할 수 있다
- 테스트를 모두 통과한 시점이 바로 코드를 완성한 시점이다
- 켄트 벡은 테스트부터 작성하는 습관을 바탕으로 테스트 주도 개발(Test-Driven-Development TDD)이란 기법을 창시했다.
- TDD에서는 테스트를 작성하고, 이 테스트를 통과하게끔 코드를 작성하고, 결과 코드를 최대한 깔끔하게 리팩터링 하는 과정을 짧은 주기로 반복한다
- 테스트-코딩-리팩터링
첫 번째 테스트
- mocha라는 테스트 프레임워크 사용
- 첫 단계에서는 테스트에 필요한 데이터와 객체를 뜻하는 픽스처fixture를 설정
- 샘플 지역 정보로부터 생성한 Province 객체를 픽스처로 설정
- 두 번째 단계에서는 이 픽스처의 속성들을 검증하는데 여기서는 주어진 초기값에 기초하여 생산 부족분을 정확히 계산했는지 확인
- 샘플 수익 계산
- Byzantium 10(비용) * 9(생산량) = 90
- Attalia 12(비용) * 10(생산량) = 120
- Sinope 10(비용) * 6(생산량) = 60
- 수요 30 - 생산량 (9 + 10 + 6) = 부족분 5
- 생산량(25) * 가격(20) = 500 - 수익(270) = 230
- 샘플의 총 생산 25개, 수요 30 → 부족분 5개 결과 True 1 passing
- 실패해야할 상황에서는 반드시 실패하게 만들자
- 각각의 테스트가 실패하는 모습을 최소한 한 번씩은 직접 확인해본다
-
오류주입
get shortfall() { return this._demand - this.totalProduction * 2; }
- 결과: AssertionError [ERR_ASSERTION]: -20 == 5
- 30 - 50 = -20
- 자주 테스트하라. 작성 중인 코드는 최소한 몇 분 간격으로 테스트하고, 적어도 하루에 한 번은 전체 테스트를 돌려보자
- 실전에서 테스트 수는 수천 개 이상일 수 있다, 뛰어난 프레임워크를 사용한다면, 많은 테스트도 간편하게 실행할 수 있고, 실패한다면 금방 확인할 수 있다.
- 차이(Chai) 라이브러리
- 어서션 라이브러리(Assertion)
- assert
- assert.equal(asia.shortfall, 5);
- chai expect
- expect(asia.shortfall).equal(5);
- GUI 환경에서 테스트시 성공은 초록막대 Green Bar, 실패는 Red Bar가 표시되는데 실패한 테스트가 하나라도 있으면 리팩토링을 하지 말아야 한다.
- 핵심은 모든 테스트가 빠르게 통과했다는 사실을 아는 것이다
테스트 추가하기
- 테스트의 목적은 현재 혹은 향후에 발생하는 버그를 찾는데 있다
- 단순히 필드를 읽고 쓰기만 하는 접근자는 테스트할 필요가 없다
- 완벽하게 만드느라 테스트를 수행하지 못하느니, 불완전한 테스트라도 작성해 실행하는 게 낫다
- profit 테스트 추가
- expect(asia.profit).equal(230);
- 결과 passing
- get profit() { return this.demandValue - this.demandCost *2 } ← 오류 케이스 만들어서 테스트가 실패하는지 확인
- 오류를 심었다가 되돌리는 패턴은 기존 코드를 테스트하는 흔한 방식이다
- 테스트 코드에서도 중복되는 코드는 제거한다
- 픽스처 코드를 상단으로 이동하여 테스트가 공유하게 하는 방식
- 테스트끼리 상호작용하게 하는 공유 픽스처는 테스트 관련 버그를 만들 수 있어서 피해야한다
- const asia 에서 const 키워드는 asia 객체의 내용이 아니라 asia를 가리키는 참조가 상수임을 뜻한다
- 다른 테스트에서 이 공유 객체의 값을 수정하면 이 픽스처를 사용하는 다른 테스트가 실패할 수 있다
- 픽스처 코드를 상단으로 이동하여 테스트가 공유하게 하는 방식
-
코드
describe('province', function() { let asia; beforeEach(function () { asia = new Province(sampleProvinceData()); }); it('shortfall', function() { expect(asia.shortFall).equal(5); }); it('profit', function() { expect(asia.profit).equal(230); }); });
- beforeEach 구문은 각각의 테스트 바로 전에 실행되어 asia를 초기화 하기 때문에 모든 케이스가 자신만의 새로운 asia를 사용하게 된다
- 매번 생성하는 픽스처로 정말 문제가 생길 때에는 공유 픽스처 값을 어떤 테스트도 변경하지 못하도록 해야한다
- beforeEach문의 등장은 표준 픽스처를 사용한다는 사실을 알려준다
픽스처 수정하기
- 실전에서는 사용자가 값을 변경하면서 픽스처의 내용도 수정되는 경우가 흔하다
- 이러한 수정 대부분은 setter에서 이루어 지는데 대부분의 setter는 단순해서 버그가 생길 일이 없지만 Producer의 production() setter는 좀 복잡한 동작을 수행하기 때문에 테스트해 볼 필요가 있다.
- 설정한 표준 픽스처를 취해 테스트를 수행하고 픽스처가 일을 기대한 대로 처리했는지를 검증하는 패턴을 설정-실행-검증(setup-exercise-verify), 조건-발생-결과(given-when-then), 준비-수행-단언(arrange-act-assert)등으로 부른다
- 해체(teardown), 청소(cleanup)라고 하는 네 번째 단계도 있는데 명시적으로 언급하지 알을 때가 많다
- 해체 단계에서는 픽스처를 제거하여 테스트들이 서로 영향을 주지 못하게 막는다
- 생성하는데 시간이 오래 걸려 공유하는 픽스처는 해체로 해제해야만 한다
- 일반적으로 it구문 하나당 검증도 하나만 하는것이 좋다
- 앞쪽 검증을 통과하지 못하면 나머지 검증은 실행도 못하고 테스트가 실패하여 실패 원인을 파악하는 데 유용한 정보를 놓치기 쉽다
경계 조건 검사하기
- 지금 까지 작성한 테스트는 일과 의도가 순조로운 꽃길(happy path)상황에 집중하였다
- 이 범위를 벗어나는 경계 지점에서 문제가 생기면 어떤 일이 벌어지는지 확인하는 테스트도 함께 작성해야 한다
- producers와 같은 컬렉션과 마주하면 그 컬렉션이 비었을 때 어떤 일이 일어나는지 확인해보자
- producers가 없는 데이터를 만들어서 부족분 30, 수익 0인 상황 검증
- 음수를 넣어 테스트
- 부족분 -26 수익 -10
- 수익이 음수가 타당한 것인가 ?
- 최소값을 0으로 설정하거나 세터에 음수라면 에러를 던지거나 0으로 초기화 설정하는 식으로 처리
- 경계값을 테스트하면 특이 상황을 처리할 수 있다
- 문제가 생길 가능성이 있는 경계 조건을 생각해보고 그 부분을 집중적을 테스트하자
- UI로부터 문자열이나 특수 문자를 입력 받을 가능성 테스트
- .NaN
- 의도적으로 코드를 망가뜨리는 방법을 모색하여 테스트를 진행하자
- 생산자 수 필드에 문자를 대입 테스트
- 테스트 실패 발생
- mocha의 경우 테스트 실패로 처리하지만 다른 프레임워크 중에는 에러와 실패를 구분하는 경우도 있다
- 실패(failure)란 검증 단계에서 실제 값이 예상 범위를 벗어났다는 뜻
- 에러(error)는 검증보다 앞선 과정에서 발생하는 예외 상황을 말한다
- 프로그램에서는 is not a function보다는 의미 있는 오류 메시지나 로그 메시지로 처리하거나 producers를 빈 배열로 설정하는 방법도 있다
- 현재 상태를 남겨두는 경우는 입력 객체를 ( 같은 코드베이스 ) 신뢰할 수 있는 곳에서 만들어주는 경우가 속한다, 같은 코드베이스의 모듈 사이에 유효성 검사(validation check) 코드가 너무 많으면 다른 곳에서 확인한 걸 중복으로 검증하여 오히려 문제가 될 수 있다
- 반면 Json으로 인코딩된 외부에서 들어온 입력 객체는 유효한지 확인해봐야 하므로 테스트를 작성한다
- 모든 버그를 잡아낼 수 없다고 생각하여 테스트를 작성하지 않는다면 대다수의 버그를 잡을 수 있는 기회를 날리는 셈이다
- 위험한 부분에 집중하고 코드에서 처리 과정이 복잡한 부분을 찾고 함수에서 오류가 생길만한 부분을 찾아 테스트를 진행하자
끝나지 않은 여정
- 과거에는 테스트를 별도의 조직에 맡기기도 했다(실력이 조금 떨어지는)
- 이제는 뛰어난 소프트웨어 개발자라면 최우선으로 관심을 가지는 주제이다
- 테스트 용이성(testability)을 아키텍처 평가 기준으로 활용하는 사례도 있다
- 이 장에서 보여준 테스트는 단위(Unit) 테스트에 해당한다
- 단위 테스트란 코드의 작은 영역만을 대상으로 빠르게 실행되도록 설계된 테스트다
- 자가 테스트 코드의 핵심이다
- 컴포넌트 사이의 상호작용, 계층의 연동을 검사하는 테스트, 성능 문제를 다루는 테스트 등 다양한 유형의 테스트가 있다
- 한 번에 완벽한 테스트를 만들 수 없으니 반복적으로 테스트를 작성해야 한다
- 제품 코드와 함께 테스트 스위트도 지속적으로 보강해야 한다
- 버그를 발견하는 순간 버그를 명확히 잡아내는 테스트 부터 작성하는 습관을 기르자
- 버그 리포트를 받으면 가장 먼저 그 버그를 드러내는 단위 테스트부터 작성하자
- 테스트가 충분한지를 평가하는 명확한 기준은 없다
- 테스트 커버리지(test coverage)를 기준으로 삼는 사람도 있지만, 커버리지 분석은 코드에서 테스트 하지 않는 영역을 찾는 데만 도움이 될 뿐 테스트 스위트의 품질과는 크게 상관 없다