1. Jest 를 활용한 JavaScript 테스트
각 진영마다 소프트웨어 테스트를 위해서 사용할 수 있는 라이브러리들이 개발되어 있는데, 그 중 자바스크립트 진영에서는 Jest, Mocha, chai 등의 테스트 라이브러리들이 대표적으로 사용되고 있다. 이중에서 Jest가 주간 약 1800만 다운로드의 압도적인 점유율을 가지고 있으며, CRA에서도 기본적으로 Jest를 포함해서 환경을 구성해주는 등 사실상 표준으로서 사용되고 있다.
1-1. Jest 사용법
Jest는 기본적으로 *.test.* 의 형태를 가진 파일을 테스트 파일로 인식하며, 해당 파일안에 있는 코드를 실행한다. 우리가 일반적으로 소프트웨어를 테스트 하는 과정은 아래와 같은 과정을 거친다.
- 특정한 동작을 수행한다.
- 동작을 수행한 결과가 기대한 상황과 일치하는지 판단한다.
테스트 코드를 작성하는 것도 마찬가지로 테스트를 하고자 하는 동작을 수행한 뒤 그 결과가 기대한 상황과 일치하는지를 검증하는 과정을 코드로 작성하게 된다.
Jest에서는 이를 기대한 상황과 일치하는지 판단하는 함수들을 matchers라고 표현한다. 따라서 Jest의 코드는 아래와 같은 형태를 띈다.
- 특정한 동작을 수행한다.
- matcher를 통해서 실제 결과와 기대값이 맞는지를 검증한다
이때 하나의 특정한 동작을 수행하기 위해서 test() 또는 it() 함수를 활용할 수 있다.
test('two plus two is four', () => {
expect(2 + 2).toBe(4);
});
it('two plus two is four', () => {
expect(2 + 2).toBe(4);
});
위의 코드에서 보듯이 테스트는 test(”테스트 이름", callback) 의 형태를 띄게 되며, callback 안에서 원하는 동작을 수행하고 expect(실제 결과 값).matcher() 의 형태를 띈다. 하나의 콜백 안에서 여러 expect를 수행할 수 있으며, 그 중 하나라도 기대값과 일치하지 않을 경우, 해당 테스트는 실패한 것으로 간주된다.
const sum = (x,y) => x + y;
test('sum', () => {
expect(sum(2,2)).toBe(4); // 통과
expect(sum(3,1)).toBe(5); // 실패, sum test 실패
});
Jest에서 주로 사용되는 matcher
- toBe : expect의 인자가 toBe의 인자와 일치하는지를 검사
- toEqual
- Object의 경우 참조값이 다르기에, toBe를 활용할 경우 실제 각 객체의 내용이 같더라도, 일치하지 않다고 판단되게 된다. 따라서 객체를 상호 비교할 때는 toEqual 를 활용해야 한다. toEqual 은 객체의 각 요소들을 재귀적으로 검사하면서 두 객체가 동일한지 판단해준다.
const obj = {hello:"world"}; test("object eqaul", () => { expect(obj).toBe({hello:"world"}) // X expect(obj).toEqual({hello:"world"}) // O });
- toBeNull, toBeUndefined
- toBeGreaterThan, toBeGraterThanOrEqaul, toBeLessThan, toBeLessThanOrEqaul : 숫자값을 검증할 때 유용하게 사용할 수 있는 matcher
- toContain : Iterable한 객체들이 특정한 요소를 포함하고 있는지 검증할 때 사용할 수 있다.
const iterable = [1,2,3,4,5];
test("iterable contain 3", () => {
expect(obj).toContain(3)
});
- not - matcher의 기대값을 반대로 변경해준다.
test('null', () => {
const n = null;
expect(n).toBeNull();
expect(n).not.toBeUndefined();
});
2. Jest와 RTL을 이용한 리액트 테스트
Jest를 통해서 순수한 자바스크립트 코드를 테스트할 수 있게되었지만, 리액트는 UI 라이브러리기에 리액트의 동작을 순수한 Jest만으로 테스트하기에는 다소 어려움이 있다. 따라서 UI를 렌더링하는 부분을 책임지는 react-dom 라이브러리에서 제공해주는 별도의 기능들과 결합하여 테스트를 수행해야한다. 이러한 과정을 매 테스트마다 수행하기에는 다소 번거롭기에 이를 대신해서 리액트 컴포넌트를 테스트 할 때 사용할 수 있는 라이브러리들이 있다.
컴포넌트의 UI와 동작을 테스트 할 때 많이 사용되는 라이브러리로는 Enzyme와 React-Testing-Library(RTL)이 존재한다.
- Enzyme : “구현"을 테스트하는 것에 초점이 맞춰져 있는 라이브러리
- RTL : “결과"를 테스트하는 것에 초점이 맞춰진 라이브러리
이 중 테스트하고자 하는 목적에 따라서 두개를 적절하게 선택해서 사용하면 된다. 이번 글에서는 리액트 공식문서에서 권장하고 있으며, CRA에 기본 구성으로 포함된 점, npm 다운로드 수가 더 많은 점, “결과"를 테스트하는 방식을 활용하기 위해서 RTL 라이브러리에 대해 더 살펴볼 것 이다.
2-1. React Testing Library (RTL)
- 리액트 컴포넌트를 테스트 할 때는 내부에서 어떤식으로 세부적인 구현이 이루어졌는지를 테스트하는 것이 아니라, 행위에 대해서 어떤 결과가 나와야하는지에 초점을 두어야 한다는 철학을 기반으로 만들어진 라이브러리
세부적인 구현을 기반으로 테스트 한다는 것은
- “특정 버튼을 클릭하면 컴포넌트의 state가 변한다. 그리고 이게 UI에 반영된다” 처럼 동작을 기반으로 테스트를 구성하는 것
결과에 대해서 테스트를 한다는 것은
- “특정 버튼을 클릭하면 화면에 2라는 숫자가 나와야 한다"처럼 최종적으로 유저가 어떤 UI를 볼 수 있어야 하는지에 초점을 두고 테스트를 하는 것
결과를 중심으로 테스트를 작성하게 되면 컴포넌트의 겉보기 동작은 그대로 유지하며, 내부적인 구현은 얼마든지 변경할 수 있다. 예를들어 구현을 테스트 했을 경우 상태관리를 useState가 아닌 Recoil, Redux 등으로 변경했을 경우 테스트코드를 다시 작성해야 하지만, 결과를 중점으로 테스트 했을 경우 상태관리가 어떻게 바뀌든 최종적으로 버튼을 클릭했을 때 화면에 2라는 숫자가 나온다는 결과만 동일하다면 테스트코드를 변경할 필요가 없다.
RTL은 이러한 철학에 기반을 두었기에 리액트 컴포넌트를 렌더링하고, 특정 요소에 접근할 수 있게 하는 기능을 제공해줍니다. 그리고 testing-library/user-event 의 경우 유저의 행동과 마찬가지로, 특정 엘리먼트에서 이벤트를 발생시키는 기능을 제공해준다.
import {render, screen} from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import App from './App'
test('App rendering', () => {
render(<App />)
const header = screen.getByText('Hello World')
const button = screen.getByText('Click me!')
userEvent.click(button);
})
RTL은 통상 jest-dom 라이브러리와 함께 사용된다. RTL은 렌더링, 요소 접근 등의 기능을 수행해준다. 하지만 테스트를 위해서는 이 요소들이 DOM상에 존재하는지, 그리고 특정 프로퍼티를 가지고 있는지 등을 검사할 수 있어야 한다. 이는 DOM에 관련된 기능이기에 jest에서는 이러한 기능을 수행할 수 있는 matcher들을 기본적으로 포함하고 있지는 않다. 이러한 matchers를 추가하기 위해서는 jest-dom 라이브러리를 사용해야 한다. (마찬가지로 CRA에 포함되어 있음)
import {render, screen} from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import '@testing-library/jest-dom'
import App from './App'
test('App rendering', () => {
render(<App />)
const header = screen.getByText('Hello World')
const button = screen.getByText('Click me!')
userEvent.click(button);
expect(header).toBeInTheDocument();
expect(button).toBeDisabled();
})
2-2. RTL의 기본적인 활용
- screen
- 말 그대로 현재 렌더링이 진행되고 있는 화면을 의미
- DOM상에서는 document.body와 동일하다고 할 수 있다.
- DOM API와 마찬가지로 screen을 통해서 현재 화면에 렌더링된 요소들에 관련된 여러 메서드들을 확인할 수 있다. - screen.debug
- 테스트 과정에서 출력된 DOM을 확인하고 싶을 때 사용할 수 있다.
- 메서드를 호출하면 호출한 시점의 렌더링된 DOM tree를 확인할 수 있다.
때때로 테스트 과정에서 원하는 결과가 나오지 않았는데, 어디서 잘못된지 파악하기 힘든 상황이 발생할 수 있다. 그런데 테스트는 실제 브라우저에서 실행되는 것이 아니기에 브라우저의 개발자도구를 통해서 DOM 트리를 확인하는 동작이 불가능하다. 바로 이럴 때 사용할 수 있는 메서드이다.
1) 요소를 가져오는 메서드
- DOM에서 제공하는 getElementBy~~~, querySelector 등의 API와 마찬가지로 RTL에서도 렌더링된 요소들에게 접근할 수 있는 메서드들이 존재
- 이러한 메서드는 크게 3가지 종류로 구분
- getBy~~~ : 해당 요소가 현재 DOM상에 있는지 동기적으로 확인한다. 만약 찾는 요소가 없을 경우 예외를 던짐.
- findBy~~~ : 해당 요소가 현재 DOM상에 있는지 비동기적으로 확인한다. 해당 요소를 찾기 위해 일정 시간을 기다리며, 시간이 지난 후에도 찾을 수 없는 경우 예외를 던짐.
- queryBy~~~ : getBy와 동일하게 동작하지만 찾는 요소가 없을 경우 예외를 던지는 것이 아닌 null을 반환.
- 예시
- getByRole
- getByText
- getByLabelText
- getByPlaceholderText
- getByText
- getByDisplayValue
- getByAltText
- getByTitle
- getByTestId
2) userEvent
- 실제 DOM상에서 유저처럼 이벤트를 발생시키기 위해서는 testing-library/user-event 라이브러리를 사용할 수 있다.
- userEvent.이벤트명(엘리먼트) 의 형태로 활용할 수 있다.
import {render, screen} from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import '@testing-library/jest-dom'
import App from './App'
test('App rendering', () => {
render(<App />)
const button = screen.getByText('Click me!')
userEvent.click(button);
})