logo
Search검색어를 포함하는 게시물들이 최신순으로 표시됩니다.
    Table of Contents
    2장: 첫 번째 단위 테스트

    이미지 보기

    2장: 첫 번째 단위 테스트

    Jest를 사용한 단위 테스트 작성

    • 25.02.25 작성

    • 읽는 데 12

    TOC

    참고

    본 내용은 단위 테스트의 기술을 읽고 정리한 내용입니다.
    책의 내용과 함께 개인적인 의견과 생각, 학습을 담아 작성하였습니다.

    Jest

    Jest에 대하여

    jest가 테스트 파일을 찾는 규칙
    • __tests__ 폴더 내의 모든 파일
    • *.spec.js 또는 *.test.js 파일
      • test와 spec은 같은 의미이므로 취향껏 선택
      • 다만 test보다 spec이 규모적으로 큰 테스트를 의미하는 경우가 많음
    jest는 import문을 따로 안 하나요?

    jest는 자동으로 global 함수를 불러옴(import)

    테스트 파일의 위치
    • 테스트 대상 파일이나 모듈 옆에 둠
      • 테스트 대상 파일이나 모듈을 찾기 쉬움
    • 모든 테스트 파일을 별도 폴더 아래에 둠
      • 테스트에 필요한 헬퍼 파일을 테스트 폴더 근처에 둘 수 있어서 편함

    Jest의 실행

    • jest 명령어를 사용하여 테스트 실행
    • jest --watch 명령어를 사용하여 테스트 실행 후 파일 변경 감지
    • jest --coverage 명령어를 사용하여 테스트 커버리지 확인

    테스트 Skill

    USE 전략

    테스트 코드에서 이름 잘 짓는 것은 무엇보다 중요
    • Unit: 테스트하려는 대상

    • Senario: 입력 값이나 상황에 대한 설명

    • Expectation: 기댓값이나 결과에 대한 설명

    • 1줄 요약: "테스트 대상을 명시하고, 어떤 입력이나 상황이 주어지면 어떤 결과로 이어져야 하는지 간결하고 명확하게 적는다."

    • 이유: 보통 테스트 이름과 결과만 터미널에 간략히 표시하므로

    문자열 비교

    문자열 비교는 의미론적 비교를 사용하라.
    • 문자열은 화면에 보이고, 비즈니스 기획에 따라 일부 수정이 되어야 할 수도 있다.
    • 하지만 중요한 건 메시지가 담고 있는 의미
    • 텍스트 비교는 동일한 비즈니스 로직에서는 항상 같은 결과를 보장해야 한다.
    • toMatch를 쓸 거라면 정규 표현식을 사용한다.
    • toContain은 문자열 중간에 포함된 문자열을 찾을 수 있어 핵심 용어를 넣어 비교

    구역의 분리

    describe함수로 구역을 나눠라.
    • describe 함수는 테스트 그룹을 만들 수 있다.
    • reporting(terminal)에서도 더 구조적으로 구분감 있게 나온다.
    describe('테스트 대상', () => {
      test('어떤 입력이나 상황, 어떤 결과', () => {
        expect(테스트 대상).toBe(기댓값);
      });
    });
    

    그리고 이렇게 중첩 사용도 가능

    describe('테스트 대상', () => {
      describe('어떤 입력이나 상황', () => {
        test('어떤 결과', () => {
          expect(테스트 대상).toBe(기댓값);
        });
      });
    });
    

    이를 통해 위의 USE 전략을 구조적으로 분리할 수 있다.

    단, 어떤 게 더 우월하다기보다는 nested할 때와 linear할 때 더 좋은 상황을 잘 구분하여 적절히 사용한다. 중간점을 잘 찾아야 한다.

    의미론적 연결

    it 함수를 사용하라.
    • it 함수는 test 함수의 별칭
    • 둘 다 같은 기능을 수행하지만 의미론적으로 더 명료하다.
      • describe it ~~
      • it returns errors

    검증 룰렛 피하기

    검증 룰렛이란?
    • 하나의 테스트 케이스에 여러 검증을 넣은 상황
    • 어떤 검증이 유효한지 확인하기 위해 앞선 검증 코드들을 주석 처리
    • 이는 많은 혼란과 잘못된 거짓 양성을 초래
    verifier.addRule(fakeRule);
    const errors = verifier.verify('any value');
    expect(errors).toHaveLength(1);
    expect(errors[0]).toContain('fake reason');
    

    차라리 it문을 여러 개 쓰더라도 별도의 테스트 케이스로 분리하기

    it('테스트 대상', () => {
      verifier.addRule(fakeRule);
      const errors = verifier.verify('any value');
      expect(errors).toHaveLength(1);
    });
    
    it('테스트 대상', () => {
      verifier.addRule(fakeRule);
      const errors = verifier.verify('any value');
      expect(errors[0]).toContain('fake reason');
    });
    

    그런데... 중복 코드가 좀 많다?

    beforeEach 함수

    중복을 제거하는 beforeEach 함수

    각 테스트 케이스가 실행되기 전에 한 번씩 실행
    describe('테스트 대상', () => {
      let verifier;
    
      beforeEach(() => {
        verifier = new Verifier();
      });
    
      describe('어떤 입력이나 상황', () => {
        let fakeRule, errors;
    
        beforeEach(() => {
          fakeRule = new FakeRule();
          errors = verifier.verify('any value');
        });
    
        it('어떤 결과1', () => {
          expect(errors).toHaveLength(1);
        });
    
        it('어떤 결과2', () => {
          expect(errors[0]).toContain('fake reason');
        });
      });
    });
    

    beforeEach 함수의 주의사항

    공유 상태에 주의하라

    • Jest는 단위 테스트를 병렬로 실행
    • 따라서 공유되는 상태를 만들면 테스트 간에 영향을 줄 수 있다.

    스크롤 피로감에 주의하라

    • 중복 코드를 없애기 위해 상위 scope로 중복코드를 이동
    • 때문에 말단인 it문에서는 전체 흐름을 알기 어려움(코드의 분산)

    팩토리 함수

    팩토리 함수의 사용

    • beforeEach 함수를 사용하여 발생한 스크롤 피로감과 코드의 분산의 해결책
    • 복잡한 코드의 추상화, 그리고 이의 추상화
    // 원소 단위
    const makeVerifier = () => new PasswordVerifier();
    const passingRule = () => ({ passed: true, reason: '' });
    const failingRule = () => ({ passed: false, reason: 'fake reason' });
    
    // 분자 단위
    const makeVerifierWithPassingRule = () => {
      const verifier = makeVerifier();
      verifier.addRule(passingRule());
      return verifier;
    };
    
    const makeVerifierWithFailingRule = () => {
      const verifier = makeVerifier();
      verifier.addRule(failingRule());
      return verifier;
    };
    
    // 조직 단위
    describe('v8 PasswordVerifier', () => {
      describe('성공하는 상황', () => {
        it ('결과1', () => {
          const verifier = makeVerifierWithPassingRule();
          expect(...);
        });
    
        it ('결과2', () => {
          const verifier = makeVerifierWithPassingRule();
          expect(...);
        })
      });
    
      describe('실패하는 상황', () => {
        it ('결과1', () => {
          const verifier = makeVerifierWithFailingRule();
          expect(...);
        });
    
        it ('결과2', () => {
          const verifier = makeVerifierWithFailingRule();
          expect(...);
        });
      });
    });
    

    팩토리 함수의 장점

    • beforeEach 함수를 사용한 듯한 유지보수성
    • 코드의 분산 감소(it문에서 모든 정보 파악 가능)
    • 스크롤 피로감 감소

    다양한 입력값을 받는 테스트 리팩터링

    test.each(it.each)
    다양한 입력값을 받는 테스트를 쉽게 작성할 수 있게 해준다.

    describe('테스트_대상', () => {
      it('결과1', () => {
        const result = 테스트_대상(input1); 
        expect(result).toBe(expected1);
      });
    
      it('결과2', () => {
        const result = 테스트_대상(input2);
        expect(result).toBe(expected2);
      });
    
      ...
    });
    

    이런 반복 코드를 아래와 같이 함축한다.

    describe('테스트_대상', () => {
      test.each([
        [input1, expected1],
        [input2, expected2],
        ...
      ])('input: %s, expected: %s', (input, expected) => {
        expect(테스트_대상(input)).toBe(expected);
      });
    });
    

    단, 이 방식은 복잡해질 경우 오히려 가독성을 해칠 수 있으니 주의가 필요

    테스트 카테고리 설정

    --testPathPattern 사용

    jest가 테스트를 찾는 방식을 정의(참고: jest 공식 문서)

    별도의 jest.config.js 파일

    • 각 테스트 카테고리에 대해 별도의 jest.config.js 파일 생성
    • 각 파일에 testRegex 옵션과 다른 설정을 지정
    // jest.config.integration.js
    var config = require('./jest.config');
    config.testRegex = 'integration/.*\\.js$';
    module.exports = config;
    
    // jest.config.unit.js
    var config = require('./jest.config');
    config.testRegex = 'unit/.*\\.js$';
    module.exports = config;
    

    이렇게 하면 각 테스트 카테고리에 대해 별도의 npm 스크립트를 만들 수 있다.

    // package.json
    {
      "scripts": {
        "test:unit": "jest -c jest.config.unit.js",
        "test:integration": "jest -c jest.config.integration.js"
      }
    }
    

    총평

    jest를 맛보다

    그동안 토스 채용 프로세스나 우아한 테크캠프 등 단위 테스트 라이브러리로 jest를 '소비'해오기만 했어요. 그런데 이제는 생산자로서 테스트를 실제로 짜고 이를 활용할 수 있게 된 것이 뿌듯하고 좋은 시작이 되었어요. 그렇게 어렵지도 않은데 그동안 우선순위에 밀려서 개인 개발에서도, 회사 개발에서도 포기했는데, 이렇게라도 접해보니 즐겁습니다. 좋은 구조들과 방법론 좋은 코드들을 고민해볼 수 있는 시작이 되길 바라요.

    중용이 어렵다

    describe를 사용하여 구조적으로 계층을 나눈 nested한 구조, 그리고 test를 사용해 linear하게 나누는 것을 비교하는 챕터에서 결국 결론은, '중간을 잘 찾고 지킨다'였어요. 그런데 이게 참 어렵다고 느껴집니다. 여기부터는 사람들 사이의 생각과 선호가 있으니까요. 많이 짜보고 경험을 쌓으면 어떤 상황에 어떻게 하는 게 유리할 지 잘 선택할 수 있게 되겠죠.

    왜 테스트 환경이 안 되지

    jest 실행이 책에서 하라는대로 따라했는데도 잘 되지 않아 많이 헤맸어요. 사실 지금은 독서 스터디 프로젝트에 미리 세팅된 vitest 설정에 따라 실습을 해보고 있어 당장은 필요 없지만 왜 그런지 이유를 찾아볼 필요가 있겠습니다.

    profile

    FE Developer 박승훈

    노력하는 자는 즐기는 자를 이길 수 없다