프론트엔드에서의 테스트는 사용자가 프로그램에서 수행할 주요 비즈니스 로직이나 모든 경우의 수를 고려해야 하며, 이 과정에서 굳이 프론트엔드 코드를 알 필요는 없다. 즉 블랙박스 형태로 테스트가 이뤄지며, 코드가 어떻게 됐든 상관없이 의도한 대로 작동하는지 확인하는데 초점이 맞춰져 있다.
React Testing Library는 Dom Testing Library를 기반으로 만들어졌다.
Dom Testing Library는 jsdom을 기반으로 하고 있는데, jsdom이란 순수하게 자바스크립트로 작성된 라이브러리로, HTML이 없는 자바스크립트만 존재하는 환경에서 HTML과 DOM을 사용할 수 있도록 해주는 라이브러리다.
해당 라이브러리를 기반으로 동일한 원리로 리액트 기반 환경에서 리액트 컴포넌트를 테스팅할 수 있는 라이브러리가 바로 리액트 테스팅 라이브러리다.
Node.js는 assert라는 모듈을 기본적으로 제공하며, 이 모듈을 이용하면 다음과 같이 작동하도록 만들 수 있다.
const assert = require('assert');
function sum(a,b){
return a+b
}
assert.equal(sum(1,2),3)
assert.equal(sum(1,2),4) // AssertionError
좋은 테스트 코드는 다양한 테스트 코드가 작성되고 통과하는 것 뿐만 아니라 어떤 테스트가 무엇을 테스트하는지 일목요연하게 보여주는 것도 가능하다.
이러한 테스트의 기승전결을 완성해주는 것이 테스팅 프레임워크이다. 자바스크립트에서 유명한 테스팅 프레임워크로는 Jest, Mocha, Karma, Jasmine 등이 있다.
math.js
function sum(a,b){
return a+b
}
module.exports = {
sum,
}
math.test.js
const {sum} = require('./math');
test('두 인수가 덧셈이 되어야 한다.', () => {
expect(sum(1,2)).toBe(3)
})
test('두 인수가 덧셈이 되어야 한다.', () => {
expect(sum(2,2)).toBe(3) // 에러
})
기본적으로 리액트에서 컴포넌트 테스트는 다음과 같은 순서로 진행된다.
프로젝트 생성
App.test.tsx가 App.tsx에서 테스트하는 내용은 다음과 같이 요약할 수 있다.
<App /> 을 렌더링한다.expoet(linkElement).toBeInTheDocument()라는 어설션을 활용해 2번에서 찾은 요소가 documnet 내부에 있는지 확인한다. HTML 요소가 있는 지 여부를 확인하는 방법은 다음과 같다.
정적 컴포넌트
예시
import StaticComponent from './index';
beforeEach(() => {
render(<StaticComponent />)
})
describe('링크 확인', () => {
it('링크가 3개 존재한다.', () => {
const ul = screen.getByTestId('ul');
expect(ul.children.length).toBe(3)
})
it('링크 목록의 스타일이 square다.', () => {
const ul = screen.getByTestId('ul');
expect(ul).toHaveStyle('list-style-type: square;')
})
})
동적 컴포넌트
예시
import { InputComponent } from '.';
describe('InputComponent 테스트', () => {
const setup = () => {
const screen = render(<InputComponent />)
const input = screen.getByLabelText('input')
const button = screen.getByText(/제출하기/i)
return {
input,
button,
...screen,
}
}
it('Input의 초기값은 빈 문자열이다.', () => {
const { input } = setup();
expect(input).toHaveAttribute('maxlength', '20')
})
it('아이디를 입력하면 버튼이 활성화된다.', () => {
const { button, input } = setup();
const inputValue = 'helloworld'
expect(input.value).toEqual(inputValue)
expect(button).toBeEnabled()
})
it('버튼을 클릭하면 alert가 해당 아이디로 표시된다.', () => {
const alertMock = jest
.spyOn(window,’alert’)
.mockImplementataion((_:string) => undefined)
const { button, input } = setup();
const inputValue = 'helloworld'
userEvent.type(input, inputValue)
fireEvent.click(button)
expect(alertMock).toHaveBeenCalledTimes(1)
expect(alertMock).toHaveBeenCalledWith(inputValue)
})
})
비동기 이벤트가 발생하는 컴포넌트
서버 응답에서 오류가 난 경우나 데이터를 불러오는 등 여러가지 상황을 위해 MSW라는 라이브러리르 사용한다. Node.js나 브라우저에서 모두 사용할 수 있는 라이브러리로, 브라우저에서는 서비스 워커를 활용해 실제 네트워크 요청을 가로채는 방식으로 모킹을 구현한다.
Node.js에서는 https나 XMLHttpRequest의 요청을 가로채는 방식으로 작동한다.
Node.js나 브라우저에서는 fetch 요청을 하는 것과 동일하게 네트워크 요청을 수행하고, 이 요청을 중간에 MSW가 감지하고 미리 준비한 모킹 데이터를 제공하는 방식이다.
const server = setupServer(
rest.get('/todos/:id', (req, res, ctx) => {
const todoId = req.params.id
if (Number(todoId)) {
return res (ctx.json({...MOCK_TODO_RESPONSE, id: Number(todoId) }))
} else {
return res(ctx.status (404))
}
}),
)
beforeAll(() => server.listen())
afterEach(() => server.resetHandlers())
afterAll(() => server.close())
useEffectDebugger
다만 이는 props가 변경되는 것만 확인이 가능하다. 부모 컴포넌트가 리렌더링되는 경우 useEffectDebugger로 확인할 수 없다.
프론트엔드 코드는 사용자의 입력이 매우 자유롭기 때문에 모든 상황을 커버해 테스트를 작성하기란 불가능하다. 따라서 테스트 코드를 작성하기 전에 생각해봐야 할 최우선 과제는 애플리케이션에서 가장 취약하거나 중요한 부분을 파악하는 것이다. 가장 핵심이 되는 부분부터 테스트코드를 하나씩 작성해 나가는 것이 중요하다.
테스트 코드는 소프트웨어의 코드를 100% 커버하기 위해, 혹은 테스트 코드가 모두 그린 사인을 보기 위해 작성하는 것이 아니다. 개발자가 간순 코드 작성만으로 쉽게 이룰 수 없는 목표인 소프트웨어 품질에 대한 확신을 얻기 위해 작성하는 것임을 기억하자!