Explanation
오늘은 제목 처럼 react-testing-library를 이용한 간단한 테스트 환경과 몇가지 예시?를 적어보려 합니다!
(대략 오늘의 프로젝트 구성은, typescript, react, webpack, jest, react-testing-library 입니다.)
늘 그렇듯 포스팅에 사용된 코드는 깃헙에서 확인하실 수 있습니다.
https://github.com/falsy/blog-post-example/tree/master/react-testing-library
1. 패키지 설치
순서가 좀 멋대로긴 한데.. 우선 우리 바벨 친구들 먼저 설치해줄게요.
$ npm i -D @babel/core @babel/preset-env @babel/preset-react @babel/preset-typescript
다음으로 리액트를 설치해주고
$ npm i react react-dom
$ npm i -D @types/react @types/react-dom
타입스크립트와 웹팩도 설치해주고
$ npm i -D typescript webpack webpack-cli webpack-dev-server
이렇게 하나하나 적다보니, 분명히 전에는 package.json의 디펜던시 부분 그냥 복.붙.했었는데 오늘은 왜 이렇게 적고 있는 걸까요.
(이상함을 느꼈지만 이미 시작했으니...)
이어서 웹팩에서 사용할 플러그인과 로더를 설치해 줄게요.
$ npm i -D html-webpack-plugin babel-loader html-loader
이제 거의 다 왔습니다! 끝으로 jest와 react-testing-library를 설치해줄게요
$ npm i -D jest @types/jest @testing-library/react @testing-library/jest-dom @testing-library/user-event
2. 패키지 설정
패키지 설정들은 아주아주 간단하게 보편적으로(아마도?)
// .babelrc
{
"presets": [
"@babel/preset-typescript",
"@babel/preset-env",
"@babel/preset-react"
]
}
타입스크립트 설정
// tsconfig.json
{
"compilerOptions": {
"target": "es2015",
"lib": ["dom", "dom.iterable", "esnext"],
"skipLibCheck": true,
"strict": false,
"forceConsistentCasingInFileNames": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "node",
"resolveJsonModule": true,
"jsx": "react-jsx",
"declaration": true,
"declarationDir": "types",
"outDir": "dist",
"allowSyntheticDefaultImports": true,
"baseUrl": ".",
"experimentalDecorators": true
},
"include": [
"src/**/*.*"
],
"exclude": [
"node_modules"
]
}
웹팩 설정
// webpack.config.js
const HTMLWeebPackPlugin = require('html-webpack-plugin')
module.exports = {
entry: ['./src/index.tsx'],
module: {
rules: [
{
test: /\.(js|ts)x?$/,
exclude: /node_modules/,
use: {
loader: 'babel-loader'
}
},
{
test: /\.html$/,
use: [
{
loader: 'html-loader',
options: { minimize: true }
}
]
}
]
},
resolve: {
extensions: ['.tsx', '.ts', '.jsx', '.js']
},
plugins: [
new HTMLWeebPackPlugin({
template: './src/index.html',
filename: './index.html'
})
],
devServer: {
host: 'localhost',
historyApiFallback: true
}
}
3. 테스트 코드 작성
// src/__test__/main.test.tsx
import React from 'react'
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에서 hello를 클릭하면 world의 출력을 토글할 수 있다.', async () => {
render(
<App />
)
const user = userEvent.setup()
// hello 라는 텍스트가 문서에 있고
expect(screen.getByText('hello')).toBeInTheDocument()
// text 라는 클래스를 가진 엘리먼트는 없다.
expect(document.getElementsByClassName('text').length).toBe(0)
// hello 라는 텍스트를 클릭하면
await user.click(screen.getByText('hello'))
// text 라는 클래스를 가진 엘리먼트가 하나 있다.
expect(document.getElementsByClassName('text').length).toBe(1)
// hello 라는 텍스트를 다시 클릭하면
await user.click(screen.getByText('hello'))
// text 라는 클래스를 가진 엘리먼트는 없다.
expect(document.getElementsByClassName('text').length).toBe(0)
})
4. 간단한 샘플 프로젝트
// src/App.tsx
import React, { useState } from 'react'
export default () => {
const [display, setDisplay] = useState(false)
const handleClickDisplay = () => {
setDisplay(!display)
}
return (
<>
<p className='toggle' onClick={handleClickDisplay}>
hello
</p>
{display && (
<p className='text'>
world
</p>
)}
</>
)
}
// src/index.tsx
import React from 'react'
import ReactDOM from 'react-dom/client'
import App from './App'
const container = document.getElementById('wrap') as HTMLElement
const root = ReactDOM.createRoot(container)
root.render(
<App />
)
5. 테스트 커맨드 추가 및 테스트
// package.json
...
"scripts": {
"start": "webpack-dev-server --mode development",
"test": "jest"
},
...
$ npm run test
하면 끝!
일 줄 알았지만,
Test environment jest-environment-jsdom cannot be found. Make sure the testEnvironment configuration option points to an existing node module.
jest-environment-jsdom 가 필요하다고 하네요! 당장 설치!
$ npm i -D jest-environment-jsdom
다시 실행
$ npm run test
하면 끝!
일 줄 알았지만(2),
The error below may be caused by using the wrong test environment, see https://jestjs.io/docs/configuration#testenvironment-string.
Consider using the "jsdom" test environment.
오류 메시지의 링크를 타고 가서 보면, 파일 상단에 주석을 넣으면 된다고 하네요!
https://jestjs.io/docs/configuration#testenvironment-string
// src/__test__/main.test.tsx
/**
* @jest-environment jsdom
*/
import React from 'react'
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에서 hello를 클릭하면 world의 출력을 토글할 수 있다.', async () => {
...
다시 실행
$ npm run test
하면 성공!
끝
이라고 하기엔.. 글의 정보가 너무 누가 칼들고 협박해서 어쩔 수 없이 급하게 쓴 글 같으니..
몇가지 상황을 조금 적어볼게요!
6. 즉각적으로 변하지 않아서 조금 기다려야 하는 경우
// src/__test__/waitFor.test.tsx
...
const handleClickDisplay = () => {
setTimeout(() => {
setDisplay(!display)
}, 500)
}
...
만약에 위처럼 이벤트가 바로 반영되지 않는 경우 테스트가 실패하게 되는데요. 이럴때는
// src/__test__/main.test.tsx
...
test('App에서 hello를 클릭하면 world의 출력을 토글할 수 있다.', async () => {
render(
<App />
)
const user = userEvent.setup()
expect(screen.getByText('hello')).toBeInTheDocument()
expect(document.getElementsByClassName('text').length).toBe(0)
await user.click(screen.getByText('hello'))
await waitFor(() => expect(document.getElementsByClassName('text').length).toBe(1))
})
이렇게 waitFor를 사용하면 변화를 기다렸다가 평가해준답니다.
변화를 무조건 기다리는 건 아니고 기본 타임아웃은 1000ms 라고 하네요!
(참고)
https://testing-library.com/docs/dom-testing-library/api-async/
7. useContext를 사용한 커스텀 훅 테스트하기
// src/__test__/context.test.tsx
/**
* @jest-environment jsdom
*/
import React, { useContext, useState } from 'react'
import { act, renderHook } from '@testing-library/react'
import '@testing-library/jest-dom'
const AppContext = React.createContext(null)
const AppProvider = ({ children }) => {
const [isBoolean, setBoolean] = useState(false)
return (
<AppContext.Provider value={{ isBoolean, setBoolean }}>{children}</AppContext.Provider>
)
}
const useAppHook = () => {
const { isBoolean, setBoolean } = useContext(AppContext)
return { isBoolean, setBoolean }
}
test('useAppHook 을 사용하여 isBoolean 값을 토글할 수 있다.', async () => {
const wrapper = ({ children }) => <AppProvider>{children}</AppProvider>
const { result } = renderHook(() => useAppHook(), { wrapper })
expect(result.current.isBoolean).toBe(false)
act(() => {
result.current.setBoolean(true)
})
expect(result.current.isBoolean).toBe(true)
})
짜잔! 이렇게 renderHook 과 act 를 사용하면 useContext를 사용한 커스텀 훅을 테스트 할 수 있답니다.
(참고)
https://react-hooks-testing-library.com/usage/advanced-hooks
8. window 객체 속성 정의하기
음... 테스트를 하다보면 window 객체의 특정 값이 필요할 때가 있는데요, 예를 들어 innerHeight 값이 필요하다면!
// src/__test__/window.test.tsx
/**
* @jest-environment jsdom
*/
import React from 'react'
import {render, screen} from '@testing-library/react'
import '@testing-library/jest-dom'
const App = () => {
return (
<p>{window.innerHeight}</p>
)
}
test('window의 innerHeight 값을 설정할 수 있다.', async () => {
window = Object.create(window)
Object.defineProperty(window, 'innerHeight', {
value: 200,
writable: true
})
render(
<App />
)
expect(screen.getByText('200')).toBeInTheDocument()
})
9. 이벤트 테스트 하기
간단하게 터치 이벤트를 만들어서 테스트 해볼게요.
// src/__test__/evnet.test.tsx
/**
* @jest-environment jsdom
*/
import React, { TouchEvent, useState } from 'react'
import { fireEvent, render, screen } from '@testing-library/react'
import '@testing-library/jest-dom'
const App = () => {
const [touchStatus, setTouchStatus] = useState('')
const handleTouchStart = (e: TouchEvent<HTMLDivElement>) => {
const touchY = e.touches[0].clientY
setTouchStatus(`start clientY: ${touchY}`)
}
const handleTouchMove = (e) => {
const touchY = e.touches[0].clientY
setTouchStatus(`move clientY: ${touchY}`)
}
const handleTouchEnd = () => {
setTouchStatus('end')
}
return (
<div
className='event-area'
onTouchStart={(e) => handleTouchStart(e)}
onTouchMove={(e) => handleTouchMove(e)}
onTouchEnd={handleTouchEnd}
>
hello world
<p>{touchStatus}</p>
</div>
)
}
test('터치 이벤트 상태의 변화에 따른 값을 테스트 할 수 있다.', async () => {
render(
<App />
)
const eventArea = document.querySelector('.event-area')
fireEvent.touchStart(eventArea, { touches: [{ clientY: 100 }] })
expect(screen.getByText('start clientY: 100')).toBeInTheDocument()
fireEvent.touchMove(eventArea, { touches: [{ clientY: 50 }] })
expect(screen.getByText('move clientY: 50')).toBeInTheDocument()
fireEvent.touchEnd(eventArea)
expect(screen.getByText('end')).toBeInTheDocument()
})



