[리팩터링 2판] - 테스트 구축하기

Lee Jeong Min·2022년 8월 19일
0

리팩터링 2판

목록 보기
4/12
post-thumbnail

리팩터링 2판의 Chatper 04를 보고 정리한 글입니다.

리팩터링 자체만으로는 부족하다. 리팩터링을 제대로 하기 위해선 실수를 잡아주는 견고한 테스트 스위트가 뒷받침 되어야 한다.

자가 테스트 코드의 가치

프로그래머들이 실제로 코드를 작성하는 시간의 비중은 그리 크지 않다.

현재 상황을 파악하거나 설계를 고민하기도 하며, 대부분의 시간은 디버깅에 쓴다.

책의 저자인 파울러는 테스트 코드 경험을 통해 몇 시간이 걸렸을지 모를 버그를 단 몇분에 해결한 경험을 공유하며 테스트의 중요성을 주장한다. 또한 테스트를 자주 수행하는 습관이 버그를 찾는 강력한 도구임을 언급한다.

테스트 스위트는 강력한 버그 검출 도구로, 버그를 찾는데 걸리는 시간을 대폭 줄여준다.

테스트를 작성하기 가장 좋은 시점은 프로그래밍을 시작하기 전이고, 테스트를 작성하다 보면 원하는 기능을 추가하기 위해 무엇이 필요한지 고민하게 만들어준다. 또한 구현보다 인터페이스에 집중하게 되며 코딩이 완료되는 시점을 정확하게 판단할 수 있게 한다.

→ 테스트를 모두 통과한 시점이 코드를 완성한 시점

테스트할 샘플 코드

사용자가 생산 계획을 검토하고 수정하도록 해주는 간단한 애플리케이션이 샘플 코드이다.

전체 애플리케이션 기능중에 비즈니스 로직 부분만 집중해서 살펴보자. 즉, 수익과 생산 부족분을 계산하는 클래스들만 살펴보고, HMTL을 생성하고 필드 값 변경에 반응하여 밑단의 비즈니스 로직을 적용하는 코드는 생략한다.

참고로 코드는 이렇게 성격에 따라 분리하는 것이 좋다.

비즈니스 로직 코드는 클래스 두 개로 구성된다.

  • 생산자를 표현하는 Producer
  • 다른 하나는 지역 전체를 표현하는 Province

Province 클래스 부분의 JSON 데이터로부터 지역 정보를 읽어오는 코드

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)));
  }

  addProducer(arg) {
    this._producers.push(arg);
    this._totalProduction += arg.production;
  }

  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);
  } // 숫자로 파싱해서 저장
}

앞 생성자의 인수로 쓸 JSON 데이터 생성하는 sampleProvinceData() 함수

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
  }
}

단순한 데이터 저장소로 쓰이는 Producer 클래스

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;
  }
}

그 외로 생산 부족분을 계산하는 코드와 수익을 계산하는 코드를 책에서 설명한다.

첫 번째 테스트

책에서는 테스트 프레임워크로 모카와 JS의 assertion 라이브러리인 차이 라이브러리를 사용한다.

describe('province', () => {
  it('shortfall', () => {
    const asia = new Province(sampleProvinceData()); // 픽스처 설정
    assert.equal(asia.shortfall, 5); // 검증
  });
});

위 테스트는 두 단계로 진행된다.

  1. 테스트에 필요한 데이터와 객체를 뜻하는 픽스처 설정
  2. 픽스처의 속성들을 검증하는 단계로 주어진 초깃값에 기초하여 생산 부족분을 정확히 계산했는지 확인

위에서 제대로 통과한 결과를 보고, 실제 코드에 실패할만한 요소를 집어넣어 실패하는지도 확인해보자.
저자는 이러한 테스트를 굉장히 자주 한다고 함.

테스트 추가하기

계속해서 테스트를 더 추가해보자. 클래스가 하는 일을 모두 살펴보고 각각의 기능에서 오류가 생길 수 있는 조건을 하나씩 테스트하는 식으로 진행한다. 그러나 public 메서드를 모두 테스트하지는 않는다.

테스트의 목적은 어디까지나 현재 혹은 향후에 발생할 버그를 찾는 데 있기 때문에 위험요인을 중심으로 작성해야 한다. 따라서 단순히 필드를 읽고 쓰기만 하는 접근자는 테스트할 필요가 없다.
→ 적은 수의 테스트만으로 큰 효과를 얻자.

총수익 계산 로직을 테스트

describe('province', () => {
  it('shortfall', () => {
    const asia = new Province(sampleProvinceData());
    expect(asia.shortfall).equal(5);
  })
  it('profit', () => {
    const asia = new Province(sampleProvinceData());
    expect(asia.profit).equal(230);
  })
})

230이라는 결과는 우선 코드가 내뿜는 결과를 믿고, 제대로 작동한다고 확인되면 총수익 계산 로직에 * 2를 덧붙여서 잘못된 값이 나오도록 해본다. 이 오류를 테스트가 걸러내는 게 확인되면, 원래 코드로 되돌린다.

→ 임시 값을 설정했다가 실제 값으로 대체하고, 오류를 심었다가 되돌리는 패턴을 사용

위 코드에서 중복되는 부분은 아래와 같이 바꿀 수 있다.

describe('province', () => {
  let asia;
  beforeEach(() => 
  it('shortfall', () => {
    const asia = new Province(sampleProvinceData());
    expect(asia.shortfall).equal(5);
  })
  it('profit', () => {
    const asia = new Province(sampleProvinceData());
    expect(asia.profit).equal(230);
  })
})

beforeEach를 사용하지 않고 단순하게 전역변수를 사용하게 되면 asia 객체의 값이 테스트에 따라 바뀌면서 결과가 달라질 수 있기 때문이다.
또한 beforeEach안의 값을 보고 코드를 읽는 사람들은 모든 테스트가 똑같은 기준 데이터로 시작한다는 사실을 쉽게 알 수 있다.

픽스처 수정하기

실전에서는 사용자가 값을 변경하면서 픽스처의 내용을 수정하는 경우가 흔하다. 위 예제의 Producer의 production() 세터 또한 복잡한 동작을 수행하기 때문에 테스트해볼 필요가 있다.

it('change production', () => {
  asia.producers[0].production = 20;
  expect(asia.shortfall).equal(-6);
  expect(asia.profit).equal(292);
});

현재 이 테스트는 it 구문 하나에서 두 가지 속성을 검증하고 있다. 일반적으로는 it 구문 하나당 검증도 하나씩만 하는 게 좋다.

앞쪽 검증을 통과하지 못하면 나머지 검증은 실행해보지 못하고 테스트가 실패하므로 실패 원인을 파악하는 데 유용한 정보를 놓치기 쉽기 때문이다.

여기서는 한 테스트로 묶어도 문제되지 않을 정도로 두 속성이 밀접하다고 판단하여 이렇게 작성했다고 함.

경계 조건 검사하기

범위를 벗어나는 경계 지점에서 문제가 생기면 어떤 일이 벌어지는지 확인하는 테스트도 함께 작성하면 좋다. 예를들어 producers와 같은 컬렉션이 비었을 때 테스트 코드를 한번 작성해보자.

// 생산자가 없는 경우
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);
  });
});

숫자형이 0일 때

it('zero demand', () => {
  asia.demand = 0;
  expect(asia.shortfall).equal(-25);
  expect(asia.profit).equal(0);
});

음수인 경우

it('negative demand', () => {
  asia.demand = -1;
  expect(asia.shortfall).equal(-26);
  expect(asia.profit).equal(-10);
});

이러한 테스트 코드를 작성해보면서 수익이 음수인 경우와 같은 특이 상황을 어떻게 처리하는 게 좋을지 생각해볼 수 있다.
문제가 생길 가능성이 있는 경계 조건을 생각해보고 그 부분을 집중적으로 테스트하자.

테스트 코드를 작성하는 도중, 기존 코드의 겉보기 동작을 변경 해야만하는 테스트 코드를 작성하게되는 순간이 온다. 이러한 경우 리팩터링은 겉보기 동작에 영향을 주지 않아야하기 때문에 이런 테스트를 작성하는 것을 저자는 추천하지 않는다. 그렇다면 테스트를 어디까지 해야 할까에 대한 의문에 너무 빠져들 필요는 없고, 특정 위험한 부분에 집중하는 게 좋다고 언급한다.

끝나지 않은 여정

책의 주제가 테스트가 아닌 리팩터링이기 때문에 테스트에 관한 내용은 여기서 마친다. 저자가 추천하는 테스트 관련 팁은 테스트를 반복적으로 진행하면서 테스트 스위트도 지속해서 보강하는 것이다. 또한 버그를 발견하는 즉시 발견한 버그를 명확히 잡아내는 테스트부터 작성하는 습관을 들이는 것이다.

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

테스트 커버리지는 테스트 스위트의 품질과는 크게 상관이 없기 때문에 이에 연연하지 말자. 테스트 스위트가 충분한지 평가하는 기준은 주관적이며 자가 테스트 코드의 목적은 '누군가 결함을 심으면 테스트가 발견할 수 있다는 믿음'을 갖게 해주는 것이다.

테스트를 너무 많이 작성할 가능성도 있지만 대부분의 경우 너무 적은 경우가 훨씬 많다.
→ 테스트를 어느정도 작성하자.

profile
It is possible for ordinary people to choose to be extraordinary.

0개의 댓글