JS 단위 테스트

윤뿔소·2024년 8월 3일

기술 | JavaScript

목록 보기
3/4
post-thumbnail

단위 테스트 할 때, 정규 표현식도 많이 쓰니 꼭 보고 오세요.

단위 테스트

소프트웨어 개발 과정에서 가장 작은 테스트 가능 단위, 즉 개별 함수, 메소드, 클래스 등과 같은 단위들을 검증하는 과정입니다.
정확성, 변경 용이성, 문서화, 디버깅 등의 효과를 기대할 수 있습니다.

테스트를 함으로써 각 단위가 독립적으로 올바르게 작동하는지 확인할 수 있습니다.

테스트 대상 단위

  1. 함수 / 메소드
  2. 클래스
  3. 모듈 / 컴포넌트 : 모듈 내의 함수와 클래스, 그리고 프론트엔드에서 사용하는 컴포넌트 등을 테스트합니다.
import { render, screen } from '@testing-library/react';
import MyComponent from './MyComponent';

// 테스트 케이스
test('renders learn react link', () => {
    render(<MyComponent />);
    const linkElement = screen.getByText(/learn react/i);
    expect(linkElement).toBeInTheDocument();
});
  1. 데이터베이스 인터페이스 : DB 쿼리, 트랜잭션이 예상대로 수행되는지 검증 테스트 합니다
const db = require('./db');

// 테스트 케이스
test('fetch user by id', async () => {
    const user = await db.fetchUserById(1);
    expect(user).toEqual({ id: 1, name: 'John Doe' });
});
  1. API 엔드포인트 : 비동기적으로 통신한 API의 결과값을 테스트합니다.
const request = require('supertest');
const app = require('./app');

// 테스트 케이스
test('GET /api/user', async () => {
    const response = await request(app).get('/api/user');
    expect(response.status).toBe(200);
    expect(response.body).toEqual({ id: 1, name: 'John Doe' });
});

테스트 기준과 유의사항

단위 테스트를 작성할 때, 아래의 기준인 Right - BICEP에 따르면 더욱 효율적입니다.

  1. Right - 결과가 올바른가?
    • 기대한 값이 산출되는지 검증.
  2. B - 모든 경계(Boundary) 조건이 CORRECT한가?
    • 입력값의 경계 조건을 테스트하여 예상한 대로 처리되는지 확인합니다.
      이는 최솟값, 최댓값, 공백, 에러 등 다양한 경계 조건을 포함합니다.
  3. I - 역(Inverse) 관계를 확인할 수 있나?
    • 함수나 변수 등 논리, 수학 로직 등의 역관계를 검사합니다.
  4. C - 다른 수단을 사용해서 결과를 교차 확인(Cross-check)할 수 있나?
    • 교차 검사 등 다른 방법을 사용해 동일한 결과를 얻을 수 있는지 확인합니다. 이를 통해 결과의 신뢰성을 높입니다.
  5. E - 에러 조건(Error condition)을 강제로 만들 수 있나?
    • 오류 조건을 강제로 발생시켜 프로그램이 예상한 대로 처리하는지 테스트합니다.
      메모리, 이미지 로딩, 로컬 시간 등
  6. P - 성능(Performance) 특성이 한도 내에 있나?
    • 코드의 성능을 테스트합니다. 성능이 요구사항을 충족하는지, 응답 시간이 적절한지 등을 확인합니다.

Jest vs Mocha

이제 테스트 라이브러리를 사용해 log만큼 정확하면서 간결한 테스트 라이브러리를 써보겠습니다.

저는 외부 라이브러리를 고를 때 첫번째로 npm 다운로드 횟수와 업데이트 횟수를 보고 고릅니다. 우선은 나에게 맞는지 검토하기 전에, 이 라이브러리를 사용하는 개발자 커뮤니티를 보고 고민을 조금 낮추는 편입니다.

image

Jest가 약 3배 이상의 다운로드 횟수를 보이고 있습니다. 하지만 업데이트는 Mocha는 3일 전, Jest는 10달 전으로 조금 고민이 됩니다.

2. 문법

문법이 의외로 중요합니다. 러닝 커브가 높냐 낮냐의 차이도 개발자 경험에 큰 영향을 주기 때문입니다.

// Jest
const Person = require('./person');

describe('Person unit tests', () => {
  let person;

  beforeEach(() => {
    person = new Person('John', 30);
  });

  it('Should be an adult', () => {
    expect(person.isAdult()).toBe(true);
  });

  it('Should be a child', () => {
    person.age = 12;
    expect(person.isAdult()).toBe(false);
  });
});

Jest는 설정이 간단하며, 테스트 러너, assertion, mocking 기능이 모두 포함되어 있어 별도의 설정 없이 바로 사용할 수 있습니다. 문법도 직관적이어서 초보자들이 쉽게 배울 수 있습니다.

// Mocha
const chai = require('chai');
const expect = chai.expect;
const Person = require('./person');

describe('Person unit tests', () => {
  let person;

  beforeEach(() => {
    person = new Person('John', 30);
  });

  it('Should be an adult', () => {
    expect(person.isAdult()).to.be.true;
  });

  it('Should be a child', () => {
    person.age = 12;
    expect(person.isAdult()).to.be.false;
  });
});

Mocha는 Chai, Sinon과 같은 추가 라이브러리를 사용해야 하므로 설정이 복잡할 수 있습니다.
하지만 더 많은 유연성을 제공하며, 다양한 테스트 요구 사항에 맞게 커스터마이징할 수 있습니다.

테스트 예시

여기서는 경로를 만들어주고, 현재 경로의 파일을 관리해주는 Path 클래스를 테스트합니다. Path 클래스의 다양한 메소드를 테스트하고, 각 메소드가 올바르게 동작하는지 검증합니다.

import { Path } from './index.js';
import fs from 'fs';

// jest

describe('Unix : Path 클래스 테스트', () => {
  // https://www.daleseo.com/jest-before-after/
  beforeAll(() => {
    // 테스트용 파일 생성
    fs.writeFileSync(
      '/Users/taeyeon/Desktop/Progamming/Coding_Test/Naver/test1.txt',
      'Hello, world!',
    );
    fs.writeFileSync(
      '/Users/taeyeon/Desktop/Progamming/Coding_Test/Naver/test2.txt',
      'Hello, world!',
    );
    fs.writeFileSync(
      '/Users/taeyeon/Desktop/Progamming/Coding_Test/Naver/test3.txt',
      'Different content',
    );
  });

  afterAll(() => {
    // 테스트용 파일 삭제
    fs.unlinkSync(
      '/Users/taeyeon/Desktop/Progamming/Coding_Test/Naver/test1.txt',
    );
    fs.unlinkSync(
      '/Users/taeyeon/Desktop/Progamming/Coding_Test/Naver/test2.txt',
    );
    fs.unlinkSync(
      '/Users/taeyeon/Desktop/Progamming/Coding_Test/Naver/test3.txt',
    );
  });

  test('Path Class 검증', () => {
    const p = new Path('/home/user/boost/camp/challenge/day8/problem.md');
    expect(p.isWindows).toBe(false);
    expect(p.root).toBe('/');
    expect(p.base).toBe('problem.md');
    expect(p.ext).toBe('.md');
    expect(p.name).toBe('problem');
    expect(p.absoluteString).toBe(
      '/home/user/boost/camp/challenge/day8/problem.md',
    );
  });

  test('appendComponent : 경로에 요소를 추가할 수 있어야 한다', () => {
    const p = new Path('/home/user/boost/camp.txt');
    p.appendComponent('New Folder');
    expect(p.pathString).toBe('/home/user/boost/New Folder/camp.txt');
    expect(p.absoluteString).toBe('/home/user/boost/New Folder/camp.txt');
  });

  test('deleteLastComponent : base를 제외한 마지막 경로 요소를 제거할 수 있어야 한다', () => {
    const p = new Path('/home/user/boost/camp/challenge/day8/problem.md');
    p.deleteLastComponent();
    expect(p.pathString).toBe('/home/user/boost/camp/challenge/problem.md');
    expect(p.absoluteString).toBe('/home/user/boost/camp/challenge/problem.md');
  });

  test('existFile : 파일 존재 여부를 확인할 수 있어야 한다', () => {
    const p = new Path(
      '/Users/taeyeon/Desktop/Progamming/Coding_Test/Naver/test1.txt',
    );
    expect(p.existFile).toBe(true);
  });

  test('fileSize : 파일 크기를 반환할 수 있어야 한다', () => {
    const p1 = new Path(
      '/Users/taeyeon/Desktop/Progamming/Coding_Test/Naver/test1.txt',
    );
    expect(p1.fileSize).toBe(13);
    const p3 = new Path(
      '/Users/taeyeon/Desktop/Progamming/Coding_Test/Naver/test3.txt',
    );
    expect(p3.fileSize).toBe(17);
  });

  test('compareFileSize : 파일 크기를 비교할 수 있어야 한다', () => {
    const p1 = new Path(
      '/Users/taeyeon/Desktop/Progamming/Coding_Test/Naver/test1.txt',
    );
    const p2 = new Path(
      '/Users/taeyeon/Desktop/Progamming/Coding_Test/Naver/test2.txt',
    );
    expect(p1.compareFileSize(p2)).toBe(true);
  });

  test('compareFileName : 파일 이름을 비교할 수 있어야 한다', () => {
    const p1 = new Path(
      '/Users/taeyeon/Desktop/Progamming/Coding_Test/Naver/test1.txt',
    );
    const p2 = new Path(
      '/Users/taeyeon/Desktop/Progamming/Coding_Test/Naver/test1.txt',
    );
    expect(p1.compareFileName(p2)).toBe(true);
  });

  test('compareFileContent : 파일 내용을 비교할 수 있어야 한다', () => {
    const p1 = new Path(
      '/Users/taeyeon/Desktop/Progamming/Coding_Test/Naver/test1.txt',
    );
    const p2 = new Path(
      '/Users/taeyeon/Desktop/Progamming/Coding_Test/Naver/test2.txt',
    );
    const p3 = new Path(
      '/Users/taeyeon/Desktop/Progamming/Coding_Test/Naver/test3.txt',
    );
    expect(p1.compareFileContent(p2)).toBe(true);
    expect(p1.compareFileContent(p3)).toBe(false);
  });

  test('유효성 검사 - 예외 처리 : 특수문자가 포함된 경로는 예외를 발생시켜야 한다', () => {
    expect(() => new Path('/home/user/boost/camp<.txt')).toThrow(
      '경로에는 공백, 숫자, 영어, 한글, 마침표, 슬래시 및 백슬래시만 포함될 수 있습니다.',
    );
    expect(() => new Path('/home/user/boost|camp.txt')).toThrow(
      '경로에는 공백, 숫자, 영어, 한글, 마침표, 슬래시 및 백슬래시만 포함될 수 있습니다.',
    );
  });

  test('한글과 공백이 포함된 경로를 지원해야 한다', () => {
    const p = new Path('/home/user/boost/캠프.txt');
    expect(p.base).toBe('캠프.txt');
    expect(p.name).toBe('캠프');
    expect(p.ext).toBe('.txt');
    expect(p.absoluteString).toBe('/home/user/boost/캠프.txt');
  });
});

describe('Window : Path 클래스 테스트', () => {
  test('Path Class 검증', () => {
    const p = new Path(
      'C:\\home\\user\\boost\\camp\\challenge\\day8\\problem.md',
    );
    expect(p.isWindows).toBe(true);
    expect(p.root).toBe('C:\\');
    expect(p.base).toBe('problem.md');
    expect(p.ext).toBe('.md');
    expect(p.name).toBe('problem');
    expect(p.absoluteString).toBe(
      'C:\\home\\user\\boost\\camp\\challenge\\day8\\problem.md',
    );
  });
});

추가 : 테스트 전/후로 실행 시켜줄 함수

아주 간편한 함수가 여러개 있습니다.

beforeAll : 테스트가 시작되기 전에 실행됩니다.
위 예시에서는 테스트용 파일을 생성합니다.
afterAll : 모든 테스트가 완료된 후 실행됩니다.
위 예시에서는 테스트용 파일을 삭제합니다.
beforeEach : 각 테스트 전에 실행됩니다. 필요한 경우 테스트 환경을 초기화합니다.
afterEach : 각 테스트 후에 실행됩니다. 테스트 환경을 정리합니다.

참고: File, Path 구성 요소

  • 볼륨 또는 드라이브: 파일 시스템의 특정 볼륨이나 드라이브를 나타냅니다.
    • ex) Windows의 드라이브 문자 = C:
  • 디렉터리 이름: 파일이 위치한 디렉터리를 나타냅니다. 디렉터리는 계층 구조를 이루어 파일을 정리합니다.
  • 선택적 파일 이름: 파일의 이름을 나타냅니다. 파일 이름은 확장자를 포함할 수 있습니다.
  • 볼륨 구분 기호: Windows에서 드라이브 문자를 나타내기 위해 :를 사용합니다.
    • ex) C:
  • 디렉터리 구분 문자: Unix 기반 시스템에서는 /를 사용하고, Windows에서는 \\를 사용합니다.
    • ex) C:\Users\User\file.txt

절대 경로와 상대 경로

  • 절대 경로: 파일 시스템의 루트부터 시작하는 경로입니다.
    • ex) /home/user/file.txt, C:\\Users\\User\\file.txt
  • 상대 경로: 현재 작업 디렉터리를 기준으로 한 경로입니다. 상대 경로에서는 .(현재 디렉터리)와 ..(상위 디렉터리)를 사용합니다.
    • ex) ./file.txt, ../file.txt

경로 요소 구분자

  • 경로 요소 구분자 : Unix에서는 :를 사용하여 경로 요소를 구분하고, Windows에서는 ;를 사용합니다.
  • 여러 개의 경로를 한 번에 나열할 때 사용합니다. 운영 체제가 명령어나 프로그램을 실행할 때 특정 디렉터리에서 해당 실행 파일을 찾는 데 사용됩니다.
    • ex) Unix : /usr/bin:/bin:/usr/sbin:/sbin , Windows: C:\\Windows;C:\\Program Files

UNC 경로

네트워크 경로를 나타내는 형식입니다. 일반적으로 네트워크상의 리소스에 접근할 때 사용됩니다. UNC 경로는 \\\\서버이름\\공유폴더 형식을 가집니다.

ex) \\\\server\\share\\file.txt

URL과 URI

  • URL(Uniform Resource Locator): 웹 상의 리소스를 식별하는 표준 주소 체계입니다. URL은 프로토콜, 호스트, 경로 등을 포함합니다.
    • ex) https://www.example.com/path/to/resource
  • URI(Uniform Resource Identifier): 리소스를 식별하는 통합 식별자입니다. URI는 URL을 포함하며, URL이 URI의 한 종류입니다.
    URI는 리소스를 식별하기 위한 다양한 방식을 포함합니다.
    - ex) example.com, urn:isbn:0451450523
profile
코뿔소처럼 저돌적으로

0개의 댓글