개발자맛 치즈볼
thumbnail
Study

[리팩터링 2판 스터디] 4장. 테스트 구축하기

2024.05.12.

자가 테스트 코드의 중요성

모든 테스트를 완전히 자동화하고 그 결과까지 스스로 검사하게 만들어야 한다.

테스트는 리팩터링에 반드시 필요한 토대일 뿐 아니라, 그 자체로도 프로그래밍에 중요한 역할을 한다.

테스트 스위트는 강력한 버그 검출 도구로, 버그를 찾는데 걸리는 시간을 대폭 줄여준다.
테스트 스위트부터 갖춘 뒤에 리팩터링을 하고, 리팩터링 하는 동안에도 필요하다면 계속해서 테스트를 추가하는 게 좋다.
또한, 테스트 코드를 구축하는 것으로 그치지 말고 테스트를 자주 수행하는 습관을 들여야 한다.

TDD (테스트 주도 개발)

프로그래밍을 시작하기 전이 테스트를 작성하기 가장 좋은 시점이다.

TDD: 다음 세가지 과정을 짧은 주기로 반복하는 것

  1. 처음엔 통과하지 못할 테스트를 작성함
  2. 이 테스트를 통과하게끔 코드를 작성함
  3. 결과 코드를 최대한 깔끔하게 리팩터링

테스트를 잘 하는 법

버그 리포트를 받으면 가장 먼저 그 버그를 드러내는 단위 테스트부터 작성하자.

1. 실패해야 할 상황에서는 반드시 실패하게 만들어라

테스트에 실패하도록 일시적으로 코드에 오류를 주입해서 테스트가 의도대로 잘 작성되었는지 한번씩 확인을 거쳐야 한다.

기댓값 넣기

처음부터 기댓값을 제대로 계산해서 넣을 필요는 없다.
먼저 임의의 값을 넣고 테스트를 수행한 다음, 프로그램이 내놓은 실제 값으로 대체하면 된다.

  1. 임시 값으로 테스트 기댓값 설정하기
  2. 실제 값으로 대체하기
  3. 코드에 임시로 오류 심기
  4. 실패를 확인하면 코드 되돌리기

2. 자주 테스트해라

완벽하게 만드느라 테스트를 수행하지 못하느니, 불완전한 테스트라도 작성해 실행하는 게 낫다.

작성중인 코드는 최소한 몇 분 간격으로 테스트하고, 적어도 하루에 한 번은 전체 테스트를 돌려 보아야 한다.

3. beforeEach 와 같은 표준 설정 루틴을 활용하라

여러 테스트에 중복되는 픽스처*가 있다면 테스트 바깥 범위로 그냥 끌어내지 말고 beforeEach 구문을 활용해야 한다.

* 테스트 픽스처 (Test Fixture): 테스트를 수행하기 전에 필요한 상태나 환경을 설정하는 것

describe('province', function() {
  // const asia = new Province(sampleProvinceData()); <-- 잘못된 코드

  let asia;
  
  beforeEach(function() {
    asia = new Province(sampleProvinceData());
  });

  it('shortfall', function() {
    expect(asia.shortfall).equal(5);
  });

    it('profit', function() {
    expect(asia.profit).equal(230);
  });
});
(예시 코드는 Mocha 를 사용)

주석처럼 작성하면 테스트끼리 픽스처를 공유하게 된다. 한 테스트가 공유 객체의 값을 수정하면 이 픽스처를 사용하는 또다른 테스트가 실패할 수 있다. 즉 테스트를 실행하는 순서에 따라 결과가 달라질 수 있다는 것이다.

그러므로 beforeEach 구문으로 각각의 테스트 전에 초기화를 진행해서 모든 테스트를 독립적으로 구성해야 한다.

beforeEach를 사용하지 않고 각각의 it 블록에 픽스처를 똑같이 넣을수도 있지만,
beforeEach를 사용하면 describe 블록 안의 모든 테스트가 똑같은 기준 데이터로부터 시작한다는 사실을 쉽게 알 수 있다.

  • 불변이 확실한 픽스처는 공유하기도 하지만, 가장 선호하는 방식은 매번 새로 만드는 것임
  • 매번 픽스처를 생성한다 해서 눈에 띄게 느려지는 일은 거의 없음
    • 정말 문제가 될 때 공유 픽스처를 사용하기도 하지만, 어떠한 테스트도 건드릴수 없게 함

4. it 구문 하나당 하나씩만 검증해라

여러 속성을 한 테스트에서 검증한다면 실패 원인을 파악하기 어려워진다.
한 테스트로 묶어도 문제되지 않을 정도로 여러 속성이 매우 밀접하게 연관되어 있을 때만 묶는다.

5. 경계 조건을 집중적으로 테스트해라

문제가 생길 가능성이 있는 경계 조건을 생각해보고 그 부분을 집중적으로 테스트하자.

인풋 값이 비어있을 때나 최대/최소 값을 넣었을 때, 혹은 타입을 다르게 넣었을 때 등등 경계를 확인하는 테스트를 작성하면 이러한 특이 상황을 어떻게 처리하는게 좋을지 생각해볼 수 있다. 그리고 코드를 작성한 후에는 의도한 대로 잘 처리하는지 확인할 수 있다.

어느 정도까지 테스트를 해야 하는가

어차피 모든 버그를 잡아낼 수는 없다고 생각하여 테스트를 작성하지 않는다면 대다수의 버그를 잡을 수 있는 기회를 날리는 셈이다.

이것저것 테스트를 너무 많이 작성해도 의욕이 떨어져 나중엔 테스트 코드를 작성하지 않게 될 수 있다.
그러므로 아래와 같은 위험한 부분에 집중하는 것이 좋다.

  • 코드에서 처리 과정이 복잡한 부분
  • 함수에서 오류가 생길만한 부분

또한 테스트 때문에 개발 속도가 느려지거나, 제품 코드보다 테스트 코드를 수정하는 데 시간이 더 걸린다면 테스트를 과하게 작성한건 아닌지 의심해 봐야 한다.

테스트 스위트가 충분한지 평가하는 기준

* 테스트 스위트(Test Suite): 테스트 케이스들을 하나로 묶은 것

테스트 커버리지를 테스트를 충분히 했다는 기준으로 삼을 수 있지만, 이는 코드에서 테스트하지 않은 영역을 찾는 데만 도움될 뿐 테스트 스위트의 품질과는 크게 상관이 없다.

테스트 스위트를 평가하는 기준은 주관적이며, 예시로 다음과 같은 것들을 기준으로 할 수 있다.

  • 누군가 결함을 심으면 테스트가 발견할 수 있다는 믿음
  • 리팩터링 후 테스트를 다 통과한 것만으로 리팩터링 과정에서 버그가 하나도 없다고 확신할 수 있는지
© CheeseB, Powered By Gatsby.