티스토리 뷰

해당 글은 github goldbergyoni 님의 javascript-testing-best-practices 를 번역하여 작성한 글 입니다. 번역 과정중 오류가 있을 수 있으니 댓글을 통해 피드백 주시면 감사하겠습니다.

적절하게 테스트 더블을 사용하라


테스트 더블테스트 코드를 편하게 작성할 할 수 있도록 해주는 도구 입니다. 하지만 어플리케이션 내부 구현 코드와 커플링 된다는 문제가 있기 때문에 남용하는 것은 권장하지 않습니다. 테스트 더블을 적절하게 사용하려면 요구사항 기능을 테스트하는대에 사용하는지를 고려하면 됩니다. 만약 요구사항 기능을 테스트하는대에 사용하는게 아니라면 해당 테스트는 화이트박스 테스트일 가능성이 높습니다.

 

예를 들어, 결제 서비스가 중단되었을 때 앱이 합리적으로 동작하는지를 테스트하기 위해 결제 서비스를 stub 하고 '응답 없음'을 반환하게 하거나. spy 를 사용하여 해당 서비스가 중단되었을 때 메일 전송을 시도하는지를 테스트하는 것은 요구사항 기능을 테스트하는 것이기 때문에 올바르게 테스트 더블을 사용한 경우 입니다. ("결제를 저장할 수 없는 경우 메일 발송")

 

하지만 요구사항과 관련 없이 특정 메소드가 정확한 파라미터로 어떻게 호출되었는지를 검사하는 경우 바람직하지 못한 사용예 입니다. 테스트 더블을 남용하는 경우 프로덕션 코드가 변경될 때 마다 테스트 코드도 변경해 주어야 할 가능성이 높기 때문에 테스트 코드가 오히려 짐이되는 상황이 발생하게 됩니다.

 

권장하지 않는 예제

유효한 product 가 제거되었을 때 DAL의 메소드 올바른 파라미터로 호출되었는지를 검증하는 테스트 코드입니다. 이는 요구사항과는 전혀 상관 없이 테스트 더블을 사용하였기 때문에 권장하지 않는 예제 입니다.

it("When a valid product is about to be deleted, ensure data access DAL was called once, with the right product and right config", async () => {
    //Assume we already added a product
    const dataAccessMock = sinon.mock(DAL);
    //hmmm BAD: testing the internals is actually our main goal here, not just a side-effect
    dataAccessMock.expects("deleteProduct").once().withArgs(DBConfig, theProductWeJustAdded, true, false);
    new ProductService().deletePrice(theProductWeJustAdded);
    dataAccessMock.verify();
});

권장하는 예제

유효한 product 가 제거되었을 때 이메일을 전송하는지를 검증하는 테스트 코드 입니다. 요구사항 기능을 테스트 하는 것에 집중한 코드이지만, 불가피하게 Emailer의 sendEmail 메소드 인터페이스에 의존을 가지고 있습니다.

it("When a valid product is about to be deleted, ensure an email is sent", async () => {
    //Assume we already added here a product
    const spy = sinon.spy(Emailer.prototype, "sendEmail");
    new ProductService().deletePrice(theProductWeJustAdded);
    //hmmm OK: we deal with internals? Yes, but as a side effect of testing the requirements (sending an email)
});

현실적인 입력 데이터를 사용하라


프로덕션 코드에서는 특별하거나 특이한 입력 데이터로 인해 버그가 들어나는 경우가 종종 발생합니다. 테스트 코드를 작성할 때 일반적인, 이상적인 데이터만을 이용하여 테스트하면 테스트는 쉽게 통과 합니다. 하지만 실제 제품에서 사용자의 실수나 해커에 의해 비정상적인 값이 입력되면 버그가 발생할 수 있습니다.

 

따라서 테스트할 때 사용하는 입력 데이터를 현실적인 값으로 사용할 수록 버그나 취약점을 조기에 발견할 가능성이 높아집니다. Facker 와 같은 라이브러리를 사용해서 실제 입력 데이터와 유사한 다양한 데이터를 생성할 수 있습니다. 간단한 예로 Facker 는 전화번호, 사용자 이름, 카드 번호, 회사 이름, 'lorem ipsum' 텍스트 등 현실적인 데이터를 생성할 수 있습니다. 또한 입력 데이터를 랜덤화 하여 테스트하는 unit을 늘릴 수 있습니다.

 

권장하지 않는 예제 : 비현실적인 데이터로 인해 쉽게 통과하는 테스트 케이스

const addProduct = (name, price) =>{
  const productNameRegexNoSpace = /^\S*$/;//no white-space allowd

  if(!productNameRegexNoSpace.test(name))
    return false;//this path never reached due to dull input

    //some logic here
    return true;
};

test("Wrong: When adding new product with valid properties, get successful confirmation", async () => {
    //The string "Foo" which is used in all tests never triggers a false result
    const addProductResult = addProduct("Foo", 5);
    expect(addProductResult).toBe(true);
    //Positive-false: the operation succeeded because we never tried with long
    //product name including spaces
});

권장하는 예제 : 현실적인 랜덤 입력 데이터

it("Better: When adding new valid product, get successful confirmation", async () => {
    const addProductResult = addProduct(faker.commerce.productName(), faker.random.number());
    //Generated random input: {'Sleek Cotton Computer',  85481}
    expect(addProductResult).to.be.true;
    //Test failed, the random input triggered some path we never planned for.
    //We discovered a bug early!
});

Property 기반 테스트로 많은 입력 조합을 테스트


일반적으로 테스트 코드를 작성할 때 입력하는 데이터 샘플은 매우 적은 양으로 구성합니다. 이렇게 적은 양의 입력자료 샘플은 입력자료를 겨우 몇가지 조합만을 테스트 하게 됩니다. (method(‘’, true, 1), method(“string” , false” , 0))

 

하지만 실제 소프트웨어 제품에서는 API가 5개의 파라미터만 입력받더라도 수천 가지의 조합으로 코드가 수행될 수 있습니다. 또한 이러한 수천 가지의 조합중 어떤 조건이 버그를 발생시키는지 알 수 없습니다. (Fuzz Testing)

 

property-based testing 는 하나의 테스트 코드만 작성하고도 수천 가지 조합의 파라미터를 자동으로 입력한 뒤 올바른 결과를 받는지를 검증할 수 있습니다. 또한 이를 통해 발견 가능한 버그의 범위를 넓힐 수 있습니다.

 

대표적인 property-based testing 라이브러리는 js-verify, testcheck-js, fast-check 등이 있으며 jest. mocha 등 테스트 프레임워크에 상관 없이 사용할 수 있습니다.

 

권장하는 예제 : mocha-testcheck 를 사용한 테스트

require('mocha-testcheck').install();
const {expect} = require('chai');

describe('Product service', () => {
  describe('Adding new', () => {
    //this will run 100 times with different random properties
    check.it('Add new product with random yet valid properties, always successful',
      gen.int, gen.string, (id, name) => {
        expect(addNewProduct(id, name).status).to.equal('approved');
      });
  })
});

참고자료


 

테스트 더블(test double)

# 테스트 더블(test double) + 의미테스트 작성시, 테스트 대상 코드와 상호작용하는 객체 + 역할테스트 대...

blog.naver.com

 

goldbergyoni/javascript-testing-best-practices

📗🌐 🚢 Comprehensive and exhaustive JavaScript & Node.js testing best practices (August 2019) - goldbergyoni/javascript-testing-best-practices

github.com

 

댓글
댓글쓰기 폼
Total
354,596
Today
15
Yesterday
658
링크
«   2019/11   »
          1 2
3 4 5 6 7 8 9
10 11 12 13 14 15 16
17 18 19 20 21 22 23
24 25 26 27 28 29 30
글 보관함