0. Jest란?

JS 테스트 프레임워크이다.

내가 작성한 코드가 제대로 동작하는지 확인할 때 사용한다.

여러가지 상황들을 설정하고, 그 상황에 맞는 결과가 나오는지 자동으로 테스트 해준다.

프로젝트 규모가 커질수록 테스트해야 할 양이 많아지는데

그럴때 Jest를 사용한다.

그렇다면 왜 Jest를 선택하였나?

Jest 이전에는 여러 테스트 라이브러리를 섞어서 사용했다고 한다.

Mock 함수를 만들기 위해 Sinon과 Testdouble 같은 Test Mock 라이브러리를 추가로 설치하여 사용하는 것이 그 예이다.

Jest를 사용하면 거의 모든 기능을 한 번에 지원하기 때문에 선택하였다.

1. 설치

$ npm i -D jest babel-jest @babel/core @babel/preset-env

jest와 babel-jest를 함께 설치한다.

jest로 테스트할 때 ES6이상의 문법을 사용하기 위해선 babel의 도움이 필요하다.

(babel 처리를 하지 않으면 import조차 실행되지 않는다 ㅠ)


2. 설정

2.1. jest.config.js

module.exports = {
  moduleFileExtensions: ["js", "json", "jsx", "ts", "tsx", "json"],
  transform: {
    "^.+\\.(js|jsx)?$": "babel-jest",
  },
  moduleNameMapper: {
    "^@/(.*)$": "<rootDir>/$1",
    '\\.(jpg|ico|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$':
      '<rootDir>/__mocks__/fileMock.js',
    '\\.(css|less)$': '<rootDir>/__mocks__/fileMock.js',
  },
  testMatch: [
    "<rootDir>/**/*.test.(js|jsx|ts|tsx)",
    "<rootDir>/(tests/unit/**/*.spec.(js|jsx|ts|tsx)|**/__tests__/*.(js|jsx|ts|tsx))",
  ],
  transformIgnorePatterns: ["<rootDir>/node_modules/"],
};

루트 폴더에 jest.config.js를 만들고 상기 내용을 적는다.

moduleFileExtensions, moduleNameMapper, testMatch는 테스트를 적용할 파일들을 설정한다.

moduleNameMapper를 통해 특정 파일명을 가진 리소스들은 fileMock.js로 대체하였다. (이런 처리를 하지 않으면 리소스 때문에 에러가 발생한다)

transform은 babel 처리를 설정한다.

transformIgnorePatterns는 test 제외 대상을 설정한다.

2.2. fileMock.js

module.exports = '';

이미지나 음원 파일을 통해 무엇인가 확인하는 일은 없을 것이다.

의미 없는 빈문자열을 export 하는 것으로 충분하다.

2.3. babel.config.js

module.exports = {
  presets: [
    [
      "@babel/preset-env",
      {
        targets: {
          node: "current",
        },
      },
    ],
  ],
};

루트 폴더에 babel.config.js를 만들고 상기 내용을 적는다.

바벨 적용시 preset을 적용할 수 있도록 설정한다.

2.4. package.json

"scripts": {
    "test": "jest",
  },

package.json에 test 실행시 jest가 실행되도록 작성한다.


3. Test 파일 작성 규칙

테스트파일이름.test.js로 파일을 작성하거나

__tests__ 폴더에 들어있는 test 파일들은 일괄적으로 테스트된다.


4. 테스트 방법

4.1. 기초 문법

let temp;
describe("simple test", () => {
  beforeEach(() => {
    temp = 1;
  });
  
  afterEach(() => {
    temp = 0;
  });
  
  test('1 is 1', () => {
    expect(1).toBe(1);
  });
  
  test('[1,2,3] is [1,2,3]', () => {
    expect([1,2,3]).toEqual(1);
  });
})

4.1.1 describe

describe: 테스트 단위를 묶는 가장 큰 단위이다. 테스트 시 describe에 설명된 내용으로 테스트 단위를 크게 분류 해준다.

4.1.2 test, it

test(), it()을 통해 기본 테스트를 한다.

test와 it의 기능적 차이는 없지만 it의 경우 다른 테스트 프레임워크에서 많이 사용하기 때문에 넣었다고 한다.

4.1.3 expect

expect()안에 테스트할 변수나 값을 넣는다. 이후 toBe나 toEqual을 이용해 예측 값과 비교한다.

4.1.4 toBe, toEqual

결과 예측으로 가장 많이 쓰는 문법은 toBe와 toEqual이다.

toBe는 단순 비교, toEqual은 배열이나 객체 내부까지 깊은 비교를 해준다.

4.1.3 beforeEach, afterEach

let temp;
describe("simple test", () => {
  beforeEach(() => {
    temp = 1;
  });
  
  afterEach(() => {
    temp = 0;
  });
  
  test('tmep is 1', () => {
    expect(temp).toBe(1); // true
  });
  
  test('temp is 1', () => {
    expect(temp).toBe(1); // true
  });
});

beforeEach는 test()가 실행할 때마다 실행해주는 전처리기이다.

afterEach의 경우 test()가 종료될 때마다 실행하는 후처리기이다.

따라서 위 예시에서 몇번의 테스트를 하더라도 temp는 1이 된다.

4.2 Mock 함수를 만들어 테스트

비동기 함수, 특히 API를 Call하는 함수를 테스트할 때 백엔드 서버의 상황에 따라 테스트 결과가 달라질 수 있다는 문제가 있다.

이러한 문제를 Mock 함수로 해결할 수 있다.

jest 환경에서는 fetch와 같은 함수를 제공하지 않는다.

따라서 fetch를 임의로 만들어주면 테스트 환경에서의 fetch는 내가 만든 가짜 fetch가 실행된다.
(axios의 경우에도 일단 axios를 import 후 임의로 axios 함수를 만들어 줄 수 있다)

import CommentModel from "../js/model/CommentModel";

test("데이터가 없을 경우 빈배열 리턴", async () => {
  beforeEach(() => {
    commentModel = new CommentModel();
  });
  
  global.fetch = jest.fn().mockImplementation(() => {
    return new Promise((resolve, reject) => {
      resolve({
        json: () => {
          return [];  // 빈배열 리턴
        },
      });
    });
  });

  const comments = await commentModel.getComments(1);
  expect(comments.length).toBe(0); // 리턴 값이 빈배열인지 확인
});

위 코드를 예시로 설명하겠다.

만약 commentModel안의 getComments()가 fetch 함수를 사용한다고 하자.

jest test환경 안에서 fetch를 임의의 함수로 만들어 줬기 때문에

getComments에서 fetch를 호출하더라도 내가 임의로 만든 목함수가 호출된다.

따라서 fetch 호출시 백엔드 API와 상관없이 빈배열을 리턴하도록 만들 수 있다.

만약 기존에 사용하는 라이브러리를 import해서 사용할 때는 어떻게 할 수 있을까?

jest.mock을 사용하여 해결할 수 있다.

jest.mock('electron', () => ({
  ipcRenderer: { invoke: jest.fn(), on: jest.fn() },
}));

예를들어 electron의 ipc통신 함수를 mock 함수로 만들고 싶다면

위와 같이 mock을 생성한 후 대신 리턴할 객체를 가진 함수를 넣어주면 된다.

4.3 뷰 테스트

뷰 테스트를 어떻게 해야하는지에 대해 이야기가 많다.

DOM 비교가 맞는지, layout 확인을 위해 픽셀단위 비교가 맞는지에 대한 것은 각자 판단해야할 문제라고 생각한다.

여기서는 간단히 DOM 테스트를 진행할 때 생기는 문제를 해결해보겠다.

React, Angular 같은 라이브러리나 프레임 워크를 사용한다면 JS가 DOM을 만드는 구조이기 때문에 Unit Test에 큰 문제가 없다.

문제는 HTML을 작성하고 JS에서 querySelector만으로 DOM을 조작할 때 생긴다.

이런 경우 직접 HTML을 넣고 테스트를 해야한다.

import FormView from "../js/views/FormView";

const defaultHTML = `
<header>
    <h1>Jest 테스트</h1>
</header>
<main>
  <article>
    <h2>현재 날씨</h2>
    <span>맑음</span>
  </article>
</main> 
`
describe("FormView Test", () => {
  beforeEach(() => {
    document.body.innerHTML = defaultHTML; // 먼저 html을 넣고
    formView = new FormView(); // 그 후에 querySelector를 사용하는 모듈 초기화
  });

  test("날씨가 맑은지 확인", () => {
    expect([
      formView.weather.value,
    ]).toEqual("맑음"); 
  });

});

위 코드와 같이 테스트할 HTML을 미리 넣어주면

querySelector를 사용하는 모듈을 초기화해도 문제없이 테스트 가능하다.

4.4 이벤트 객체

마우스 클릭의 좌표가 테스트에 필요한 경우가 있다.

이런 경우 MouseEvent 객체를 만들어서 사용하면 된다.

const mouseEvent = {
  clientX: 280,
  clientY: 280,
  currentTarget: {
    getBoundingClientRect: () => ({
      left: 40,
      top: 40,
    }),
  },
};

const seconds = timerController.getSeconds(
  (mouseEvent as unknown) as MouseEvent,
);

다만 TypeScript의 경우 as를 이용하여 타입 변환이 필요하다

4.5 타이머

jest에서 setTimeOut 테스를 하려면 jest.useFakeTimers()로 처리해야 한다.

또한 실제로 1초가 지난 후 테스트를 하기 위해선 jest.advanceTimersByTime(1000) 으로 딜레이를 줄 수 있다.

profile
FrontEnd Developer

0개의 댓글

Powered by GraphCDN, the GraphQL CDN