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

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

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

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

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

테스트 코드에서 이름 잘 짓는 것은 무엇보다 중요
  • 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');
});

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

각 테스트 케이스가 실행되기 전에 한 번씩 실행
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');
    });
  });
});

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

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

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

  • 각 테스트 카테고리에 대해 별도의 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를 '소비'해오기만 했어요. 그런데 이제는 생산자로서 테스트를 실제로 짜고 이를 활용할 수 있게 된 것이 뿌듯하고 좋은 시작이 되었어요. 그렇게 어렵지도 않은데 그동안 우선순위에 밀려서 개인 개발에서도, 회사 개발에서도 포기했는데, 이렇게라도 접해보니 즐겁습니다. 좋은 구조들과 방법론 좋은 코드들을 고민해볼 수 있는 시작이 되길 바라요.

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

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