TIL 102 - Test with Jest

김영현·2024년 6월 17일
0

TIL

목록 보기
113/129


썸네일 출처 : https://www.geeksforgeeks.org/testing-with-jest/

Test

시험, 검사
개발자가 만든 애플리케이션이 원하던 대로 동작하는지 검증하는 행위를 테스트라고 한다.
나아가서 애플리케이션자체가 아닌, 애플리케이션 로직일부도 테스트 가능하다.
사실 이번 TIL은 이쪽에 초점을 맞추었다.

unitTest

소스 코드의 특정 모듈이 의도된 대로 정확히 작동하는지 검증하는 절차
테스트가 필요한 이유를 나열하라면 끝도 없을 것이다.
개발자가 만든 코드가 백프로 의도한 대로동작하면 정말 좋겠지만, 그런 코드를 한 번에 제작하는 개발자는 지극히 소수가 아닐까?(감히 예측컨대 없을수도 있다)
따라서 코드가 의도치 않은 방향으로 실행되는 것을 막기위하여 테스트는 필수불가결 요소다.

개중 먼저 간단한 유닛 테스트부터 해보겠다.


Jest(With Next14)

Jest is a delightful JavaScript Testing Framework with a focus on simplicity.
출처 : https://jestjs.io/

Jest는 간단하면서 강력한 테스트 도구다. 개발 환경이 Next14라서 Next공식문서를 보고 설치를 진행하였다.

npm install -D jest jest-environment-jsdom @testing-library/react @testing-library/jest-dom
# or
yarn add -D jest jest-environment-jsdom @testing-library/react @testing-library/jest-dom
# or
pnpm install -D jest jest-environment-jsdom @testing-library/react @testing-library/jest-dom

이후 초기 설정 파일을 생성해주면 된다.

npm init jest@latest
# or
yarn create jest@latest
# or
pnpm create jest@latest

위 명령어로 생긴 초기 설정파일에 아래 코드를 덮어씌우자.

//jest.config.ts
import type { Config } from 'jest'
import nextJest from 'next/jest.js'
 
const createJestConfig = nextJest({
  // Provide the path to your Next.js app to load next.config.js and .env files in your test environment
  dir: './',
})
 
// Add any custom config to be passed to Jest
const config: Config = {
  coverageProvider: 'v8',
  testEnvironment: 'jsdom',
  // Add more setup options before each test is run
  // setupFilesAfterEnv: ['<rootDir>/jest.setup.ts'],
}
 
// createJestConfig is exported this way to ensure that next/jest can load the Next.js config which is async
export default createJestConfig(config)

자세히보니 Next파일 내부에 이미 next/jest.js라는 파일이 존재한다. 테스트까지 고려해서 제작되었구나?
참고로 jest를 설치할때 같이 설치한 jsdomnode.js환경에서 브라우저 DOM을 테스트하기 위하여 설치한 환경이다.

주의할 점은 경로별칭을 사용할 시 jest.config.ts에서 경로 별칭을 매핑해주어야한다.

getting Started

Jest공식문서를 읽어가며 지식을 쌓아보자.

아래와같은 더하기 함수가있다.

const sum = (a,b) => a+b;

아주 단순한 함수지만, 혹시몰라 테스트를 해보고싶다. 더하기 함수이니 3 + 5를 넣으면 8 을 반환해야한다.

import { sum } from "./sum"

test('3더하기 5는 8이다',()=>{
  expect(sum(3,5)).toBe(8) //expect: 예상하다
})

이렇게 작성된 테스트를 돌려보면...

(나머지 테스트는 무시해도 좋다)

아무튼 테스트가 통과되었다! 만약 실패하면 어떻게 나올까?

함수가 9를 반환해야하지만, 8을 반환했다고 나온다. 이처럼 단순한 유틸함수의 테스트는 그리 어렵지 않다.

참고로 test()대신 it()를 사용한 예시도 많은데, 둘의 기능은 같다.

아무튼 정리하자면...

  1. 함수를 불러와서
  2. 특정한 인자를 넘겨주고
  3. 함수의 반환값을 예상한 값과 일치하는지 판별한다.

이게 단위 테스트의 핵심이다.

toBe vs toEqaul

단순한 원시값을 비교할땐 toBe()를 사용해도 문제가 없다. 그러나 참조형(객체)데이터를 다룰땐 원치 않는 결과가 발생한다.
왜냐면 toBe()는 사실 Object.is()기 때문이다.

//userObj
export const userObj = (name:string) => ({name})

//test
import { userObj } from "./userObj"

test('kim과 kim은 같은 이름이다.', () =>{
  expect(userObj('kim')).toBe({name:'kim'})
})

자랑스러운 JS개발자(지망)로서 Object.is(userObj('kim'),{name:'kim'})false임을 안다. 왜냐하면 a와 b는 다른 스택을 가리키고 있기 때문이다. 그 스택들은 다른 힙을 가리키고...아무튼 요지는 메모리가 아니다.

프로그래밍 환경에서는 메모리가 다르면 다른 값이 되지만, 비즈니스적으론 두 이름을 동명으로 취급하고 싶은 경우도 있다.
단순히 프로퍼티만 따와서 비교할 수도 있지만, 내부 프로퍼티가 많다면 귀찮은 작업이 될 것이다.

그래서 내부 프로퍼티에 재귀적으로 Object.is() 를 호출하는 toEqual()을 사용하는게 좋다.

참고로 부동소수점을 비교할땐 toBeCloseTo()메서드를 사용하는 게 좋다. (10진법 => 2진법으로 인한 부동소수점 오차)

mocking

속이다, 조롱하다
단어 뜻만 보면 기분이 썩 이상하지만, 테스트에서의 모킹은 모의 객체를 뜻한다. 테스트할 모듈이 다른 모듈을 의존성으로 갖고 있을 때, 의존성으로 갖는 모듈을 테스트할때만 사용 할 수 있게 흉내내는 것이다.

예를들어 현재 rem의 기준이 되는 html의 폰트 크기를 가져오는 함수가 있다.

const getRemInPx = () =>
  parseFloat(getComputedStyle(document.documentElement).fontSize);

이 함수를 테스트하면 어떻게 될까?

import getRemInPx from "./getRemInPx";

test("get Rem In window fontsize", () => {
  expect(getRemInPx()).toBe(16)
});

예상한 결과가 아닌 NaN을 받아볼 수 있다. 왜 그런걸까?
다시 폰트크기를 가져오는 함수에 주목해보자.

document.documentElement는 문서의 루트 element를 반환한다.


출처 : https://developer.mozilla.org/en-US/docs/Web/API/Document/documentElement

그리고 getComputedStyle()메서드는 전달받은 element의 모든 CSS속성값을 담은 객체를 반환한다.

여기서 fontSize 프로퍼티를 가져와서 parseFloat()으로 부동소수점으로 만든 뒤 반환하는 함수 인 것이다.

현재 테스트 환경은 jsdom으로 형성되어있다. jsdomnode환경에서 브라우저의 DOM을 구현한 것이다.
한번 어떻게 되어있는 지 확인해보자.

export const getDocEl = () => getComputedStyle(document.documentElement)

//test
import { getDocEl } from "./getDocEl"

test('도큐먼트를 가져옵니다', () => {
  expect(getDocEl()).toEqual(document.documentElement)
})


모니터 하나에 담기지도 않을 만큼 굉장히 많은 값들이 비어있다. 개중 폰트 크기 역시 비어있다.

jest에서 사용하는 jsdom환경은 사실 정확히 구현되어있는 게 아니었구나?

이를 해결하려면 document.documentElement의 모든 스타일 값을 채워주어야할까? 아니다.
폰트크기를 가져오는 함수에서는 document.documentElement의 스타일 값 중 fontSize부분만 필요로했다.
따라서 그 부분만 테스트 할때 채워넣으면되지 않을까?

다르게 말하자면 getComputedStyle을 호출 했을 때 원하는 폰트 크기가 담긴 객체를 반환하면 되는 게 아닐까...?

export const mockGetComputedStyle = (fontSize:number) => {
  window.getComputedStyle = jest.fn().mockImplementation(() => ({
    fontSize,
  }));
};

//test
import { mockGetComputedStyle } from "../../../__mocks__/mockGetComputedStyle"
import { getDocEl } from "./getDocEl"

mockGetComputedStyle(16)
  
test('도큐먼트를 가져옵니다', () => {
    expect(getDocEl()).toBe(16)
})

getComputedStyle()을 호출하였을때 {fontSize:인자로 받아온 크기}를 반환하게 만들었다.
이때 사용한 jest의 mockImplementation()은 말 그대로 테스트에 필요한 함수를 대체(모킹)하는 용도로 사용한다.
html(루트 엘리먼트)의 폰트크기는 기본 16px이다. 따라서 16을 반환해야하니, 성공한 셈이다.

여기서 한가지 의문은 위 방식 대신document.documentElement의 스타일 중 fontSize프로퍼티를 채워넣는게 바람직한게 아닐까?
내가 사용한 방식은 정말 편법에 가까운듯 해서...😥

아직 테스트에 대해 잘 모르기도 하고 모킹 자체가 생소하니 일단 대체할수 있다라는 것만 알아두고 넘어가보자.
(알게 되면 꼭 보충하겠습니다!!)

describe, before, after

위 방식처럼 mocking이 필요한 테스트의 경우, 한 테스트 파일 내 서로 다른 mocking이 필요하다거나, mocking없이 연관된 여러 테스트 함수가 존재할 수 있다.
테스트 파일을 나누어도 되지만, 한 파일내에서 그룹화할 수 있다면 보기도 편하고 수정도 용이하다.

이때 사용할 수 있는게 describe키워드다.

const myBeverage = {
  delicious: true,
  sour: false,
};

describe('my beverage', () => {
  test('is delicious', () => {
    expect(myBeverage.delicious).toBeTruthy();
  });

  test('is not sour', () => {
    expect(myBeverage.sour).toBeFalsy();
  });
});

이렇게 연관된 테스트를 진행할때 하나의 그룹으로 묶어두면 좋다.

또한 descrbie스코프라고 볼 수 있다.

import { mockGetComputedStyle } from "../../../__mocks__/mockGetComputedStyle";
import getRemInPx from "./getRemInPx";

describe('getRemInPx',()=>{
  beforeEach(() => {
    mockGetComputedStyle(16)
  });
  
  afterEach(() => {
    jest.restoreAllMocks();
  })
  
  test("get Rem In window fontsize", () => {
    expect(getRemInPx()).toBe(16)
  });
});

describe키워드 내 테스트함수가 실행되기 전beforEach()를 통해 모킹을 진행하고, afterEach()를 통해 테스트 함수 실행 후 모킹된 데이터를 초기화해준다. 그렇게 하면 남아있는 모킹으로 인한 의도치않은 버그를 피할 수 있다.

참고로 afterEach(), beforeEach()메서드는 각 테스트 함수마다 같이 실행된다. 따라서 describe그룹 내에서 딱 한번만 필요한 경우 afterAll(), beforeAll()메서드를 이용하여 그룹 내 실행전, 실행후 한 번만 실행하게 할 수 있다.


느낀점

프로젝트를 진행할때 각 키워드를 잘 모른채로 단순한 유틸리티 함수만 test()expect()를 이용하여 테스트했었다.
하지만 오늘부로 모킹도 할수 있고 그룹화도 할 수 있게되었다. 테스트는 항상 필요하다고 생각하고 있었는데 무섭고 귀찮아서 미룬 경향이 강했다. 이젠 두렵지않다 테스트!

다음편에서는 RTL을 이용하여 컴포넌트 테스트를 진행해보겠습니다!

profile
모르는 것을 모른다고 하기

0개의 댓글