본문 바로가기

TDD

단위 테스트: 기본 개념

반응형

Jest란

  • Jest는 JavaScript 테스팅 프레임워크로, 주로 Node.js 애플리케이션을 테스트하는 데 사용된다.
  • Facebook에서 개발했으며, 여러 가지 강력한 기능과 도구들을 제공하여 테스트 작성과 실행을 쉽게 만든다

장점

  • 간편한 설정: Jest는 설정이 거의 필요 없도록 설계되었고 기본 설정으로도 대부분의 프로젝트에서 바로 사용할 수 있다.
  • 스냅샷 테스트: Jest는 컴포넌트의 출력이나 함수의 반환값을 스냅샷으로 저장하고, 이후 테스트 실행 시 이 스냅샷과 비교하여 변경 사항을 감지한다
  • 모의 함수와 모듈: Jest는 테스트 중 특정 함수나 모듈을 모의(mock)하여 테스트 환경을 제어할 수 있는 기능을 제공한다
  • 비동기 코드 테스트: 콜백, 프로미스, async/await 등을 포함한 비동기 코드를 쉽게 테스트할 수 있는 방법을 제공한다
  • 병렬 테스트 실행: 여러 테스트를 병렬로 실행하여 테스트 속도를 높인다
  • 코드 커버리지: Jest는 테스트 커버리지 보고서를 생성하여 코드베이스에서 테스트되지 않은 부분을 쉽게 확인할 수 있다
  • 타임 트래블 기능: 테스트 중 타이머를 제어하고, 시간을 빠르게 감거나 느리게 할 수 있는 기능을 제공한다

단점

  • 학습 곡선: Jest의 다양한 기능과 설정 옵션들을 모두 이해하고 활용하는 데 시간이 걸릴 수 있다. 특히 처음 사용하는 경우, 설정과 모의(Mock) 기능 등을 익히는 데 어려움을 겪을 수 있다
  • 퍼포먼스: 대규모 프로젝트에서 많은 테스트 케이스를 실행할 때 성능 문제가 발생할 수 있다. Jest는 기본적으로 병렬 테스트 실행을 지원하지만, 여전히 테스트 시간이 오래 걸릴 수 있다.
  • 모듈 모킹의 한계: Jest의 모듈 모킹 기능이 강력하지만, 특정 시나리오에서는 예상치 못한 동작을 할 수 있습니다. 특히 복잡한 모듈 의존성을 가진 프로젝트에서는 모킹이 어려울 수 있다.
  • 생태계 종속성: Jest는 주로 React와 관련된 프로젝트에서 많이 사용되지만, 다른 프레임워크나 라이브러리와의 호환성 문제도 있을 수 있다. 예를 들어, Angular 프로젝트에서는 다른 테스팅 프레임워크(예: Karma, Jasmine)가 더 적합할 수 있다.
  • 디버깅의 어려움: 테스트 실패 시 원인을 파악하고 디버깅하는 과정이 복잡할 수 있다. 특히 비동기 코드나 복잡한 상태 관리를 테스트할 때 문제가 발생할 수 있다.
  • 커뮤니티 지원: Jest는 많은 사용자와 활발한 커뮤니티를 가지고 있지만, 특정 문제에 대한 해결책을 찾는 데 어려움을 겪을 수 있다.

 

 

활용 실습


초기 환경 구성

install

npm i -D jest

 

package.json

{
  "name": "project name",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "jest --watchAll"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "dependencies": {
    "express": "^4.19.2",
    "jest": "^29.7.0"
  }
}

 

run

npm run test

명명 규칙

  • 파일 확장자
    • *.test.js/ts, *.spec.js/ts와 같은 확장자를 사용한다
  • 테스트 파일 위치
    • 테스트 파일은 소스 파일과 같은 디렉토리에 저장한다
    • 같은 모듈의 계층에 생성
  • 보통의 클래스와 구분하기 위해서 “Test”라는 접미사를 붙이는 것을 당연한 규칙으로 자리 잡고 있다
  • describe("", () => { const somethingClassTest = new SomethingClass(); // ... test code });

 

 

단언 메서드(Assertion Method)


  • Test Case의 실행 결과를 판별해주는 메서드

주요 메서드(JUnit과 비교)

  • toBe ↔️ assertEquals : 원시 값이 동일한지 확인
  • toEqual ↔️ assertEquals : 객체나 배열의 값이 동일한지 확인
    • 객체 비교 시 assertEquals가 아니라 equals 사용
  • toBeNull ↔️ assertNull : null인지 확인
  • toBeUndefined ↔️ assertNull : undefined인지 확인
  • toBeDefined ↔️ assertNotNull : undefined가 아닌지 확인
  • toBeTruthy ↔️ assertTrue : true로 평가될 수 있는지 확인
  • toBeFalsy ↔️ assertFalse : false로 평가될 수 있는지 확인
  • toContain ↔️ (assertTrue, contains) : 배열이나 문자열이 특정 요소를 포함하는지 확인
  • toThrow ↔️ assertThrows : 함수가 예외를 던지는지 확인
  • throw ↔️ fail : 테스트를 실패 시키고자 할 때 사용
  • 그 외 Jest Assertion Method
    • toBeGreaterThan
    • toBeLessThan
    • toHaveLength
    • toMatch
    • toMatchObject

 

 

실습

toBe

describe("기본 문법 테스트", () => {
  test("기본 테스트", () => {
    const actual = 1;
    const expected = 2;
    expect(actual).toBe(expected); // failed
  });
});

 FAIL  test/syntax.test.js
  기본 문법 테스트
    ✕ 기본 테스트 (8 ms)

  ● 기본 문법 테스트 › 기본 테스트

    expect(received).toBe(expected) // Object.is equality

    Expected: 2
    Received: 1

      3 |     const actual = 1;
      4 |     const expected = 2;
    > 5 |     expect(actual).toBe(expected);
        |                    ^
      6 |   });
      7 | });
      8 |

      at Object.toBe (test/syntax.test.js:5:20)

Test Suites: 1 failed, 1 total
Tests:       1 failed, 1 total
Snapshots:   0 total
Time:        0.524 s, estimated 1 s

-------------------------------------------------------------------------

describe("기본 문법 테스트", () => {
  test("기본 테스트", () => {
    const actual = 2;
    const expected = 2;
    expect(actual).toBe(expected);
  });
});

PASS  test/syntax.test.js
  기본 문법 테스트
    ✓ 기본 테스트 (6 ms)

Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total
Snapshots:   0 total
Time:        0.469 s, estimated 1 s
Ran all test suites.

Watch Usage: Press w to show more.

toThrow

class Syntax {
  upperCase(str) {
    if (!str) throw new Error("No string provided");
    return str.toUpperCase();
  }
}

test("throw 테스트", () => {
  const syntax = new Syntax();

  expect(() => syntax.upperCase()).toThrow("No string provided");
  expect(() => syntax.upperCase("hello")).not.toThrow();
  expect(syntax.upperCase("hello")).toBe("HELLO");
});

 PASS  test/syntax.test.js
  기본 문법 테스트
    ✓ 기본 테스트 (5 ms)
    ✓ throw 테스트 (7 ms)

Test Suites: 1 passed, 1 total
Tests:       2 passed, 2 total
Snapshots:   0 total
Time:        0.371 s, estimated 1 s
Ran all test suites.

Watch Usage: Press w to show more.

throw

describe("기본 문법 테스트", () => {
  test("throw 테스트", () => {
    try {
      const syntax = new Syntax();
      syntax.upperCase("");
    } catch (error) {
      expect(error.message).toBe("Error");
    }
  });
});

 PASS  test/syntax.test.js
  기본 문법 테스트
    ✓ 기본 테스트 (3 ms)
    ✓ toThrow 테스트 (5 ms)
    ✓ throw 테스트

Test Suites: 1 passed, 1 total
Tests:       3 passed, 3 total
Snapshots:   0 total
Time:        0.501 s, estimated 1 s
Ran all test suites.

Watch Usage: Press w to show more.

 

 

 

Test LifeCycle(JUnit과 비교)


  • Class Level Setup beforeAll ↔️ @BeforeAll
    • 한 클래스의 모든 테스트가 메서드가 실행되기 전에 특정 작업을 수행해야 하는 경우
    • 모든 테스트가 실행되기 전에 한 번 실행됨
  • Setup beforeEach ↔️ @BeforeEach
    • 테스트를 실행하는데 필요한 준비 작업을 할 때 사용됨
    • 임시 파일을 생성한다거나 테스트 메서드에서 사용할 객체를 생성할 수 있다
    • 각 테스트가 실행되기 전에 실행됨
  • Test Execution it(test) ↔️ @Test
    • 개발자가 의도했던 테스트 결과가 나오는지 확인하는 단계
  • Clean-up afterEach ↔️ @AfterEach
    • 테스트 종료 후, 정리할 것이 있을 때 사용
    • 사용한 리소스를 반환하거나 임시로 사용한 파일을 삭제하는 기능을 여기서 진행
    • 각 테스트가 실행된 후에 실행됨
  • Class Level Clean-up afterAll ↔️ @AfterAll
    • 모든 테스트가 끝나는 시점
    • 테스트 환경에 부가적으로 필요했던 인스턴스의 리소스 반환이나 종료 등을 여기서 진행
    • 모든 테스트가 실행된 후에 한 번 실행됨
class Lifecycle {
  constructor() {
    this.data = [];
  }

  add(item) {
    this.data.push(item);
  }

  remove(item) {
    this.data = this.data.filter((i) => i !== item);
  }

  get() {
    return this.data;
  }

  clear() {
    this.data = [];
  }
}

describe("LifeCycle Test", () => {
  let lifecycle;

  // Class Level Setup
  beforeAll(() => {
    console.log("beforeAll");
  });

  // Setup
  beforeEach(() => {
    console.log("beforeEach");
    lifecycle = new Lifecycle();
  });

  // Cleanup
  afterEach(() => {
    console.log("afterEach");
  });

  // Class Level Cleanup
  afterAll(() => {
    console.log("afterAll");
  });

  // Test Execution
  test("add test", () => {
    lifecycle.add(1);
    lifecycle.add(2);
    expect(lifecycle.get()).toEqual([1, 2]);
  });
});

// beforeAll → beforeEach → (Test Execution) → afterEach →  afterAll 순으로 실행
-----------------------------------------------------------------------------
 PASS  test/syntax.test.js
 PASS  test/lifecycle.test.js
  ● Console

    console.log
      beforeAll

      at Object.log (test/lifecycle.test.js:28:13)

    console.log
      beforeEach

      at Object.log (test/lifecycle.test.js:33:13)

    console.log
      afterEach

      at Object.log (test/lifecycle.test.js:39:13)

    console.log
      afterAll

      at Object.log (test/lifecycle.test.js:44:13)

Test Suites: 2 passed, 2 total
Tests:       4 passed, 4 total
Snapshots:   0 total
Time:        0.518 s, estimated 1 s
Ran all test suites.

Watch Usage: Press w to show more.
반응형