1. React Testing Library
React Testing Library 깃허브
React Testing Library 공식 문서
jest-dom
💡 React 컴포넌트를 사용자 입장에 가깝게 테스트할 수 있는 도구
UI 테스트에 특화된 라이브러리
E2E Test처럼 사용 가능 → 브라우저에서 사용자가 실제로 사용하듯이 테스트
🛠 기존의 도구
enzyme
사용자 입장에서 보다는 해킹하는 느낌으로 사용하도록 되어 있었음
React Testing Library도 구현 자체는 해킹하는 것처럼 되어 있지만, 사용자 입장에 가깝게 만들어짐
🤓 React Testing Library는?
웹 브라우저가 아닌 곳에서는 document를 쓸 수 없지만, 그런 것을 가능하게 해줌
jest는 node에서 돌아가는데 React Testing Library는 DOM 관련 메소드를 사용할 수 있고, 그 위에 react를 얹어서 돌리면 테스트 가능
Copy const div = document .createElement ( 'div' )
장점
render
를 이용하면 Main.tsx의 render와 비슷하지만 훨씬 쉽게 사용 가능
Copy import {render} from '@testing-library/react' ;
Copy // Main.tsx
root .render ((
< React.StrictMode >
< App />
</ React.StrictMode >
));
2. given - when - then 패턴
모든 BDD 시나리오에 있는 3가지 핵심 요소
WHEN (action, 동작 설명) : ~ 한 동작을 하면
THEN (outcome, 결과 설명) : ~ 이렇게 된다
Copy context ( 'with only one arguments' , () => {
it ( 'returns the same numbers' , () => {
// When
const result = add ( 2 );
// Then
expect ( add ( 2 )) .toBe ( 2 );
});
});
케이스를 나눠 코드를 짜기 때문에 표현력이 좋아지고, 다양한 상황에 대해 고민할 수 있음
3. 테스트 코드 작성하기
🔗 실습 링크
주의점
💡 UI는 관심사의 분리를 통해 비즈니스 로직과 분리하는 것을 권장
범용성이 커지고 다른 곳에서 터지는 것을 방지하기 위해 처음부터 UI를 되도록 간단하게 할 것
나머지 부분은 모듈을 모킹하거나 백엔드 부분은 MSW를 사용해 처리하기
컴포넌트의 인터페이스 점검
테스트 코드(컴포넌트를 사용하는 코드)를 작성하면서 해당 컴포넌트의 인터페이스를 점검 가능
Copy // TextField.test.tsx
import {render , screen} from '@testing-library/react' ;
import TextField from './TextField' ;
test ( 'TextField' , () => {
// Given
const text = 'Tester' ;
const setText = () => {
// do nothing...
};
// When
render ((
< TextField
label = "Name"
placeholder = "Input your name"
text = {text}
setText = {setText}
/>
));
// Then
screen .getByLabelText ( 'Name' );
screen .getByPlaceholderText ( /name/ );
screen .getByDisplayValue (text);
});
기존의 문제
text 같이 범용적인 표현을 사용하지 않은 문제
테스트부터 작성 했거나 빠르게 테스트 코드를 작성 했다면, 작성하기 전 또는 직후에 문제를 발견해서 수정할 수 있었을 것
⚠️ 시간이 지나면 해당 코드에 대한 지식이 감소하고, 자신감도 감소하기 때문에 건드리기 힘든 코드가 되니 주의
BDD 스타일로 코드 수정, 입력 기능 테스트
BDD 스타일로 코드를 바꾸고, 입력 등이 잘 작동하는지 확인
1. context 분리
Copy import {render , screen , fireEvent} from '@testing-library/react' ;
import TextField from './TextField' ;
const context = describe;
describe ( 'TextField' , () => {
// Given
const label = 'Name' ;
const text = 'Tester' ;
const setText = jest .fn (); // 모킹
it ( 'renders elements' , () => {
// When
render ((
< TextField
label = {label}
placeholder = 'Input your name'
text = {text}
setText = {setText}
/>
));
// Then
screen .getByLabelText (label);
screen .getByPlaceholderText ( /name/ );
screen .getByDisplayValue (text);
});
context ( 'when user enters name' , () => {
it ( 'calls "setText" handler' , () => {
// Given
render ((
< TextField
label = {label}
placeholder = 'Input your name'
text = {text}
setText = {setText}
/>
));
// When
fireEvent .change ( screen .getByLabelText (label) , {
target : {value : 'New Name' } ,
});
// Then
expect (setText) .toBeCalledWith ( 'New Name' );
});
});
});
2. render 함수 생성, 모킹 함수 초기화 처리
describe-context로 나눠줄수록 수월하게 진행됨
Copy import {render , screen , fireEvent} from '@testing-library/react' ;
import TextField from './TextField' ;
const context = describe;
describe ( 'TextField' , () => {
const text = 'Tester' ;
const setText = jest .fn (); // 매 테스트마다 초기화 해주어야 함
beforeEach (() => {
setText .mockClear (); // → 해당 대상만 clear
// 또는 jest.clearAllMocks(); → 전부 다 clear
});
function renderTextField () {
render ((
< TextField
label = "Name"
placeholder = "Input your name"
text = {text}
setText = {setText}
/>
));
}
function inputText (value : string ) {
fireEvent .change ( screen .getByLabelText (label) , {
target : {value} ,
});
}
it ( 'renders an input control' , () => {
// When
renderTextField ();
// Then
screen .getByLabelText ( 'Name' );
});
context ( 'when user enters name' , () => {
beforeEach (() => {
// Given
renderTextField ();
});
it ( 'calls "setText" handler' , () => {
// When
inputText ( 'New Name' );
// Then
expect (setText) .toBeCalledWith ( 'New Name' );
});
});
});
반복되는 코드를 Extract Function
만약 복잡한 로직이 컴포넌트로부터 분리된다면, 여기서는 이것만 검증하면 됨
Ex. setText에 숫자만 입력받게 하고 싶다면, 컴포넌트에서 그런 로직을 구현하는 것이 아니라 테스트 할 때 그렇게 동작하도록 작성하면 됨
API 요청 코드 모킹
🔗 실습 링크
외부 의존성이 큰 코드(API 요청 등)를 작성할 경우, 해당 부분만 가짜로 구현
Copy // App.test.tsx
import {render , screen} from '@testing-library/react' ;
import App from './App' ;
jest .mock ( './hooks/useFetchProducts' , () => () => [
{
category : 'Fruits' , price : '$1' , stocked : true , name : 'Apple' ,
} ,
]);
test ( 'App' , () => {
render (< App />);
screen .getByText ( 'Apple' );
});
매번 서버를 띄우기 어렵고, 실서버를 사용하기 어려운 문제를 방지하기 위해 테스트에서만 가짜로 서버를 구현
프론트엔드는 일반적으로 백엔드와 소통하는 비중이 큼
💡 이 부분을 하나씩 가짜 구현으로 바꾸다 보면 어려운 경우가 발생 → MSW 등 다른 대안을 고려
4. Mocking
모의 객체
가짜로 적는 것
테스트를 수행할 모듈
과 연결되는 외부의 다른 서비스나 모듈
을
실제 사용하는 모듈
을 사용하지 않고 실제의 모듈을 흉내내는 가짜 모듈 을 작성 → 테스트의 효용성을 높임
자동화된 테스트를 수행하기 어려울 때 주로 사용
🛠 TDD와의 관계
테스트 주도 개발(TDD)에서는 자동화된 테스트 가 필수적인 요소 중의 하나
모의 객체를 이용하면 상당 부분의 테스트를 사용자의 개입 없이 자동화 할 수 있음
사용 예시
사용자 인터페이스(UI) 테스트 : 사용자의 반응이 필요한 테스트를 수행할 경우, 사용자가 테스트에 참여해야 하기 때문에 자동화된 테스트 수행이 어려움. 모의 객체를 이용해 사용자의 응답을 흉내내어 사용자의 개입 없이도 테스트를 수행
데이터베이스(DB) 테스트 : 자료의 변경을 수반하는 데이터베이스에 대한 작업을 테스트 하는 경우, 테스트 수행 후 매번 데이터베이스의 자료를 원래대로 돌려놔야 하는데, 모의 객체를 이용해 데이터베이스의 응답을 흉내내어 데이터의 변경 없이 테스트가 가능
5. Test fixture
Test fixture
소프트웨어를 일관되게 테스트 하기 위해 사용되는 환경
한 곳에 몰아서 다른 곳에서 사용하기 편리함
장점
각 테스트가 항상 동일한 설정으로 시작하기 때문에 테스트를 반복할 수 있음
메소드를 다른 함수로 분리하고 각 기능을 다른 테스트에 재사용할 수 있음
이전 테스트 실행에서 남은 항목으로 작업하는 대신, 알려진 초기 상태로 테스트를 미리 구성
폴더 구조
1. 직접 사용하는 경우
Copy ├── src
│ ├── App.test.tsx
│ ├── App.tsx
Copy ├── fixtures
│ ├── index.ts
│ └── products.ts
Copy // App.test.ts
import {render , screen} from '@testing-library/react' ;
import App from './App' ;
import fixtures from '../fixtures' ;
jest .mock ( './hooks/useFetchProducts' , () => () => fixtures .products);
test ( 'App' , () => {
render (< App />);
screen .getByText ( 'Apple' );
});
Copy // fixtures/products.ts
const products = [
{
category : 'Fruits' , price : '$1' , stocked : true , name : 'Apple' ,
} ,
];
export default products;
Copy // fixtures/index.ts
import products from './products' ;
export default {
products ,
};
2. mocks 폴더를 분리할 경우
복잡해지면 이 방법을 사용
Copy │ ├── hooks
│ │ ├── __mocks__
│ │ │ └── useFetchProducts.ts
│ │ └── useFetchProducts.ts
Copy // App.test.ts
import {render , screen} from '@testing-library/react' ;
import App from './App' ;
// Jest.mock('./hooks/useFetchProducts', () => () => fixtures.products);
jest .mock ( './hooks/useFetchProducts' );
test ( 'App' , () => {
render (< App />);
screen .getByText ( 'Apple' );
});
Copy // hooks/__mocks__/useFetchProducts.ts
import fixtures from '../../../fixtures' ;
// Const useFetchProducts = () => fixtures.products; // 이렇게 써도 되지만
const useFetchProducts = jest .fn (() => fixtures .products); // 모킹을 드러내기 위해 권장되는 방법
export default useFetchProducts;