icon
8장 : 테스팅 전략

테스트 커버리지와 품질 관리


지난 절에서는 NestJS에서 E2E 테스트를 구현하여 애플리케이션의 전체적인 동작을 사용자 관점에서 검증하는 방법을 알아보았습니다. 이제 8장의 마지막 절로, 작성된 테스트 코드가 얼마나 많은 실제 코드를 검증하는지 측정하는 지표인 테스트 커버리지(Test Coverage) 에 대해 다루고, 이를 활용하여 소프트웨어 품질을 관리하는 방법에 대해 살펴보겠습니다.

테스트 커버리지는 단순히 "테스트가 얼마나 많이 작성되었는가"를 넘어, "작성된 테스트가 얼마나 많은 코드 경로를 실행하는가"를 나타내는 중요한 지표입니다. 높은 테스트 커버리지는 버그 발생 가능성을 줄이고, 코드 변경에 대한 안정성을 높이는 데 기여합니다.


테스트 커버리지란?

테스트 커버리지(Test Coverage) 는 소프트웨어 테스트가 소스 코드의 특정 부분을 실행하는 정도를 측정하는 지표입니다. 일반적으로 백분율(%)로 표시되며, 테스트 스위트가 애플리케이션 코드의 얼마나 많은 부분을 실행하는지 보여줍니다.

테스트 커버리지의 주요 유형

  • 라인 커버리지(Line Coverage): 소스 코드의 전체 실행 가능한 라인 중 몇 라인이 테스트에 의해 실행되었는지 측정합니다. 가장 일반적이고 직관적인 지표입니다.
  • 함수/메서드 커버리지(Function/Method Coverage): 전체 함수/메서드 중 몇 개가 테스트에 의해 호출되었는지 측정합니다.
  • 브랜치 커버리지(Branch Coverage): if, else, switch 문과 같은 조건문에서 가능한 모든 분기(branch)가 테스트에 의해 실행되었는지 측정합니다. 논리 흐름의 모든 경로를 테스트하는 데 중요합니다.
  • 문장 커버리지(Statement Coverage): 모든 문장(statement)이 한 번 이상 실행되었는지 측정합니다. 라인 커버리지와 유사하지만, 공백이나 주석은 제외됩니다.

높은 테스트 커버리지가 중요한 이유

  • 버그 감소: 더 많은 코드 경로가 테스트되므로, 잠재적인 버그를 조기에 발견할 가능성이 높아집니다.
  • 코드 변경의 안정성: 리팩토링이나 새로운 기능 추가 시, 기존 테스트가 충분히 커버되어 있다면 회귀(Regression)를 빠르게 감지할 수 있습니다.
  • 문서화 역할: 테스트 코드는 실제 코드의 사용 예시를 보여주므로, 일종의 문서화 역할을 합니다.
  • 개발자 자신감: 개발자가 코드 변경에 대해 더 큰 자신감을 가질 수 있도록 돕습니다.

커버리지 100%가 항상 좋은 것은 아니다?

100% 커버리지는 이론적으로 완벽하지만, 모든 상황에서 항상 달성 가능하거나 바람직한 것은 아닙니다.

  • 비용과 효율: 100% 커버리지를 달성하기 위해 비즈니스 가치가 적은 코드까지 테스트하는 것은 시간과 자원의 낭비일 수 있습니다.
  • 테스트 품질: 커버리지 숫자가 높다고 해서 테스트의 품질이 높다는 것을 의미하지는 않습니다. 단순히 코드를 실행만 시킬 뿐, 올바른 시나리오나 엣지 케이스를 검증하지 않는 "쓸모없는" 테스트일 수도 있습니다.
  • 달성 불가능한 코드: 예외 처리 로직 중 도달하기 매우 어려운 코드 경로나, 실제 배포 환경에서만 발생하는 특정 환경 의존적인 코드는 테스트하기 어려울 수 있습니다.

결론: 테스트 커버리지는 코드 품질의 지표 중 하나이지, 그 자체가 목표가 되어서는 안 됩니다. 팀의 상황과 프로젝트의 중요도에 따라 적절한 커버리지 목표를 설정하고, 양적인 지표와 함께 테스트의 질적인 측면을 함께 고려해야 합니다. 일반적으로 핵심 비즈니스 로직에 대해서는 높은 커버리지를 목표로 하고, 유틸리티 함수나 간단한 Getter/Setter 등은 조금 낮은 커버리지를 허용하기도 합니다.


NestJS에서 테스트 커버리지 측정

NestJS는 Jest를 기본 테스트 러너로 사용하며, Jest는 자체적으로 강력한 커버리지 리포트 기능을 내장하고 있습니다.

커버리지 리포트 생성

NestJS 프로젝트의 package.json 파일을 보면 이미 커버리지 리포트 생성을 위한 스크립트가 정의되어 있습니다.

// package.json (부분 발췌)
{
  "scripts": {
    "test": "jest",
    "test:watch": "jest --watch",
    "test:cov": "jest --coverage", // 이 스크립트를 사용합니다.
    "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
    "test:e2e": "jest --config ./test/jest-e2e.json"
  }
}

커버리지 리포트를 생성하려면 다음 명령어를 실행합니다:

npm run test:cov

명령어를 실행하면 Jest가 모든 테스트를 실행하고, 콘솔에 다음과 같은 요약 표를 출력합니다 (프로젝트에 따라 숫자는 다르게 나타날 수 있습니다):

--------------------|---------|----------|---------|---------|-------------------
File                | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
--------------------|---------|----------|---------|---------|-------------------
All files           |   92.85 |      100 |   83.33 |   92.85 |
 app.controller.ts  |     100 |      100 |     100 |     100 |
 app.service.ts     |     100 |      100 |     100 |     100 |
 main.ts            |     100 |      100 |     100 |     100 |
 users/users.controller.ts |   71.42 |      100 |      50 |   71.42 | 19,20
 users/users.service.ts |   85.71 |      100 |      50 |   85.71 | 22-26
--------------------|---------|----------|---------|---------|-------------------

이 표는 각 파일별로 라인, 브랜치, 함수, 문장 커버리지를 보여주고, 커버되지 않은 라인 번호(Uncovered Line #s)도 알려줍니다. All files는 전체 프로젝트의 평균 커버리지입니다.

또한, 기본적으로 coverage라는 폴더가 프로젝트 루트에 생성되며, 그 안에 상세한 HTML 리포트가 포함됩니다. coverage/lcov-report/index.html 파일을 웹 브라우저로 열면, 어떤 코드 라인이 테스트되지 않았는지 시각적으로 확인할 수 있어 테스트 개선에 큰 도움이 됩니다.

(위 이미지는 Jest 공식 문서에서 가져온 예시 이미지입니다.)

빨간색으로 표시된 라인이나 분기는 테스트에 의해 실행되지 않은 부분입니다. 이 부분을 집중적으로 테스트 코드를 추가하여 커버리지를 높일 수 있습니다.


커버리지 임계값 설정 및 품질 관리

테스트 커버리지를 품질 관리의 일부로 사용하려면, 특정 커버리지 수준을 강제하는 임계값(Threshold) 을 설정할 수 있습니다. 이는 CI/CD 파이프라인에서 커버리지가 특정 기준 미만일 경우 빌드를 실패시키는 방식으로 활용됩니다.

jest.config.js 또는 jest-e2e.json에 임계값 설정

프로젝트의 jest.config.js 파일을 열어 coverageThreshold 옵션을 추가하거나 수정합니다.

// jest.config.js
module.exports = {
  moduleFileExtensions: ['js', 'json', 'ts'],
  rootDir: 'src', // 테스트를 실행할 루트 디렉토리
  testRegex: '.*\\.spec\\.ts$', // 단위 테스트 파일만 선택
  transform: {
    '^.+\\.(t|j)s$': 'ts-jest',
  },
  collectCoverageFrom: ['**/*.(t|j)s'], // 커버리지 수집 대상 파일
  coverageDirectory: '../coverage', // 커버리지 리포트가 저장될 디렉토리
  testEnvironment: 'node',
  // **커버리지 임계값 설정**
  coverageThreshold: {
    global: { // 전역 임계값
      branches: 80, // 전체 브랜치 커버리지 80% 이상
      functions: 80, // 전체 함수 커버리지 80% 이상
      lines: 80,     // 전체 라인 커버리지 80% 이상
      statements: 80, // 전체 문장 커버리지 80% 이상
    },
    // 특정 파일이나 디렉토리에 대한 임계값도 설정 가능
    './src/app/**/*.ts': { // app 모듈 내의 모든 TypeScript 파일
      lines: 90,
      functions: 90,
    },
    './src/users/users.service.ts': { // UsersService 파일
      lines: 95,
    },
  },
};
  • coverageThreshold:
    • global: 프로젝트 전체에 대한 최소 커버리지 임계값을 설정합니다.
    • 특정 파일 경로를 지정하여 해당 파일/디렉토리에 대한 개별 임계값을 설정할 수 있습니다.

이렇게 임계값을 설정한 후 npm run test:cov를 실행했을 때, 만약 설정된 임계값을 충족하지 못하면 Jest는 에러를 발생시키고 종료됩니다. 이는 CI/CD 파이프라인에서 실패로 이어져, 테스트 커버리지가 일정 수준 이하로 떨어지는 것을 방지할 수 있습니다.

품질 관리 전략에 커버리지 활용

초기 목표 설정: 프로젝트 초기에 팀과 합의하여 현실적인 커버리지 목표를 설정합니다 (예: 신규 코드 80%, 핵심 로직 90% 이상).

CI/CD 연동: 모든 코드 푸시 또는 풀 리퀘스트(Pull Request) 시 CI/CD 파이프라인에서 자동으로 테스트 커버리지를 측정하고 임계값을 검사하도록 설정합니다. 임계값 미달 시 빌드를 실패시켜 품질 저하를 방지합니다.

정기적인 모니터링: 커버리지 리포트를 정기적으로 확인하고, 커버리지가 낮은 부분을 식별하여 개선합니다.

코드 리뷰에 활용: 코드 리뷰 시 커버리지 리포트를 참고하여, 중요한 비즈니스 로직이나 복잡한 부분에 테스트가 충분한지 확인합니다.

테스트 문화 조성: 개발자들이 테스트 작성을 습관화하고, 커버리지를 코드 품질의 중요한 지표로 인식하도록 독려합니다.


테스트 커버리지는 소프트웨어 개발의 품질을 측정하고 관리하는 데 있어 매우 유용한 도구입니다. NestJS는 Jest와의 강력한 통합을 통해 이러한 커버리지 측정 및 관리를 손쉽게 할 수 있도록 지원합니다. 하지만 중요한 것은 숫자에 맹목적으로 매달리기보다, 실제 코드의 중요한 기능과 잠재적 위험 영역에 집중하여 의미 있는 테스트를 작성하는 것입니다.

이것으로 8장 "테스팅 전략"을 모두 마칩니다. 이제 여러분은 NestJS 애플리케이션에 대한 단위 테스트, E2E 테스트를 작성하고, 테스트 커버리지를 측정하여 코드 품질을 관리하는 방법을 이해하게 되었습니다. 잘 만들어진 테스트 코드는 안정적이고 유지보수하기 쉬운 애플리케이션을 만드는 데 필수적인 요소입니다.