[책/리팩토링] 4장. 테스트 구축하기

aeong98·2022년 2월 20일
0
post-thumbnail
post-custom-banner

테스트

자가 테스트 코드의 가치

  • 모든 테스트를 완전히 자동화하고 그 결과까지 스스로 검사하게 만들자
  • test suite는 강력한 버그 검츨 도구로, 버그를 찾는 데 걸리는 시간을 대폭 줄여준다.
  • 테스트 주도 개발(TDD)
    - 테스트를 작성하고, 이 테스트를 통과하게끔 코드를 작성하고, 결과 코드를 최대한 깔끔하게 리팩터링 하는 과정을 짧은 주기로 반복

테스트 과정

  1. 테스트에 필요한 데이터와 객체를 뜻하는 픽스처를 설정.
  2. 이 픽스처들의 속성들을 검증 (주어진 초깃값에 기초하여 속성을 정확히 게산했는지)
  3. 일시적으로 오류를 주입

테스트 추가하기

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

테스트 코드에서 픽스처를 공유할 수 있게 하는 바업ㅂ?

const asia= new Province(sampleProvinceData()); // 이렇게 하면 안된다.

it ('shoftfall',()=>{})...
  • 이렇게 하면 일시적인 효과는 있겠지만, 테스트 관련 버그 중 가장 지저분한 유형인 '테스트끼리 상호작용하게 하는 공유 픽스처'를 생성하는 원인이 된다. 자바스크립트에서 const 키워드는 asia 객체의 내용이 아니라 asia 를 가르키는 참조가 상수임을 뜻한다.
  • 따라서 나중에 다른 테스트에서 이 공유 객체의 값을 수정하면, 이 픽스처를 사용하는 또 다른 테스트가 실패할 수 있다.
  • 즉, 테스트를 실행하는 순서에 따라 결과가 달라질 수 있다.
describe('province', ()=>{
    let asia;
    beforeEach(()=>{
        asia= new Province(sampleProvinceData());
    })
})
  • beforeEach 구문은 각각의 테스트를 바로 전에 실행되어 asia 를 초기화하기 때문에, 모든 테스트가 자신만의 새로운 asia 를 사용하게 된다.
  • 이처럼 개별 테스트를 실행할 때마다 픽스처를 새로 만들면 모든 테스트를 독립적으로 구성할 수 있어서, 결과를 예측할 수 없어 골치를 썩는 사태를 예방할 수 있다.
  • 코드를 읽는 이들은 해당 describe 블록 안의 모든 테스트가 똑같은 기준 데이터로부터 시작한다는 사실을 쉽게 알 수 있다.

흔한 테스트 패턴

  • beforeEach 블록에서 설정한 표준 픽스처를 취해서
  • 테스트를 수행하고
  • 이 픽스처가 일을 기대한 대로 처리했는지를 검증
  • 설정-실행-검증 / 조건-발생-결과 / 준비-수행-단언

경계 조건 검사하기

  • 컬렉션이 비었을 때 어떤 일이 일어나는지 (예시에서 producers 와 같은 경우)
  • 숫자형이라면 0일 때를 검사해보기
  • 음수를 넣어보면 좋음
  • 문제가 생길 가능성이 있는 경계 조건을 생각해보고, 그 부분을 집중적으로 테스트하기

끝나지 않은 여정

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

😳예제 코드😳

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

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

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

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

module.exports = {
  sampleProvinceData,
  Province,
};

✌🏻테스트 코드✌🏻

import {Province, sampleProvinceData} from './app';


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

    it('profit', ()=>{
        expect(asia.profit).toEqual(230);
    })

    it('change production', ()=>{
        asia.producers[0].production=20;
        expect(asia.shortfall).toEqual(-6);
        expect(asia.profit).toEqual(292);
    })
    
    // 숫자형이라면, 0일때를 검사
    it('zero demand', ()=>{
        asia.demand=0;
        expect(asia.shortfall).toEqual(-25);
        expect(asia.profit).toEqual(0);
    });

    // 수요 입력란이 비어있을 때(UI 로부터 문자열을 취하고 있을 때, 필드가 비어 있을 수 있음)
    it('empty string demand', ()=>{
        asia.demand="";
        expect(asia.shortfall).toBeNaN;
        expect(asia.profit).toBeNaN;
    })
})

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).toEqual(30);
    })

    it('profit',()=>{
        expect(noProducers.profit).toEqual(0);
    })
})

describe('string for producers', ()=>{
    it('', ()=>{
        const data={
            name:"String producers",
            producers :"",
            demand:30,
            price:20,
        };
        const prov= new Province(data);
        expect(prov.shortfall).toEqual(0);
    })
})
profile
프린이탈출하자
post-custom-banner

0개의 댓글