개발하면서 한번쯤은 들어본 TDD(테스트 주도개발)
처음엔 필요성을 그다지 느끼지 못했는데 지금 생각해보면 어떤걸 테스트 해야할지 몰라서 그랬던것같기도하다.
첫 테스트할때 새로운 메소드들때문에 생소할수있지만 반복하다보면 조금씩 눈에 들어온다.
JEST와 react testing library를 공부하면서 중요하다 생각한걸 정리해보고자 한다.
목차
1. TEST & TDD
2. Testing tool (JEST, React Testing Library)
3. 리액트 테스트시 자주 사용한 주요 API 소개
개발상에서 불리는 테스트란 우리의 제품 및 서비스가 예상하는대로 동작하는지 확인하고 검증하는 용도로 사용된다.(함수, 특정기능, UI,성능, API 스펙 등)
1) Unit test
각 컴포넌트 및 기능 단위의 동작을 검증함.
단위테스트는 함수와 같이 a single piece of code를 검증하는데 도움을 준다.
함수에 인풋을 넣고 기대했던 아웃풋이 나오는지 확인하는 방식으로 단위테스트는 짧고 간단하며 매우 빨리 실행된다.
2) Integrating Test
최소 두개 이상의 클래스 또는 서브 시스템의 결합을 테스트하는 방법, 각 모듈간의 상호작용을 검증함.
일반적으로 단위 테스트만큼 많은 통합테스트는 필요하지 않지만 서로 다른 구성요소가 함께 잘 동작하는지 확인할필요가 있다.
3) End to End test
실제유저가 보는 화면을 기준으로 하는 테스트.
사용자의 브라우저환경에서 직접 값을 입력해서 테스트 할수있다.
1) Arrage : 테스트할 데이터준비
2) Act : 주어진 데이터에 값을 넣어 실행
3) Assert : 데이터 최종결과값이 예상한 값인지 확인
class Calculator {
constructor(){
this.value = 0;
}
set(num){
this.value = num;
}
clear(){
this.value = 0;
}
}
describe('Calculator', ()=>{
let cal;
beforeEach(()=>{
//assert : 테스트할 데이터(클래스)준비
cal = new Calculator()
})
it('set', ()=>{
//act : 주어진 데이터에 값을 넣어 실행
cal.set(9)
//assert : 데이터 결과값 9인지 확인
expect(cal.value).toBe(9)
})
})
1) 요구사항이 적절하게 잘 동작하는가
2) 모든 경우를 테스트 (잘못된포맷형식,Null,undefined,특수문자,잘못된이메일형식,작은숫자,큰숫자,순서가 잘못되었을때 등)
3) 역관계를 통해 결과값 확인
4) 다른 수단을 이용해서 결과값이 맞는지 확인
5) 에러상황을 잘 컨트롤 하는지(네트워크 에러시, 메모리부족시,데이터베이스 오류시 등)
6) 성능확인을 정확한 수치로 확인
TDD(Test Driven Development)란? 개발전 테스트 코드를 먼저 작성하는것.
TDD는 테스트 주도 개발의 약자로써 작은 단위의 테스트 케이스를 작성하고, 이를 실패/통과하는 코드들을 실험하고 추가하는 단계를 반복해서 구현한다. 테스트 코드를 작성함으로써 실제 코드에 대해 기대되는 바를 보다 명확하게 정의 함으로써 불필요한 설계를 피할수있고 정확한 요구사항에 집중할수있다.
1) 실제코드를 구현하기전 테스트 코드를 먼저 작성한다.
2) 테스트 코드 실행 ===> 실패 (당연히 실제코드가 없기때문에 실패한다.)
3) 이때 실패하는 테스트를 통과할 정도의 최소 실제 코드를 작성해서 성공시킨다.
4) 이를 반복하면서 코드들을 추가한다.
5) 전체적인 기능이 완성된다면 중복코드제거, 일반화등의 리팩토링을 수행한다.
1) 디버깅시간 단축
2) 문서기능 : TDD를 하게 될 경우 테스팅을 자동화 시킴과 동시에 보다 정확한 테스트 근거를 산출할수있다.
3) 재설계시간의 단축 : 개발자가 무엇을 해야하는지 분명하게 정의하고 개발을 시작하게된다. 또한 테스트 시나리오를 작성하면서 다양한 예외사항에 대해 생각할수있다. 이는 개발진행중 소프트웨어의 전반적인 설계가 변경되는 일을 방지할수있다.
4) 코드의 품질 향상
5) 코드간 의존성 낮춤
6) 보다 튼튼한 객체 지향적인 코드 생산 : TDD는 코드의 재사용 보장을 명시하므로 TDD를 통한 소프트웨어 개발시 기능별 철저한 모듈화가 이루어진다. 종속성과 의존성이 낮은 모듈로 조합되어있어 필요에 따라 추가 제거해도 소프트웨어 전체 구조에 영향을 미치지 않는다.
개발시간 증가 : 중간중간 테스트를 진행하면서 코드를 작성해야하기때문에 일반 개발방식에 비해 대략 10~30%정도 늘어난다.
1) 자바스크립트 - 자바스크립트에서 데스트시 JEST추천. JEST 관련 구글이나 네이버에 많은 정보들이 있음.
2) 리액트 - 리액트 테스트시 추천되는 툴은 JEST와 REACT Testing library가 있다.
3) 개인적인 팁
JEST는 자바스크립트 개발에 용이하며 test runner, test matcher, test mock프레임워크를 제공해주기때문에 손쉽게 테스트해볼수있다.
1) Matcher
다양한 Matcher함수가 제공된다. (toBe(),toEqual(),toHaveLength(),toContain()등)
expect('test target').Matcher(Conditions the test subject must pass);
2) Mocking Method
테스트하고자 하는 코드가 의존하는 function이나 class에 대해 모조품을 만들어 일단 테스트 하는 것을 말한다.
리액트 컴포넌트 테스트를 하면서 jest.fn(),mockImplemention(),jest.mock()을 많이 사용했다.
test("mock Test", () => {
const mockFn = jest.fn();
mockFn.mockImplementation((name) => `I am ${name}`);
mockFn("a");
mockFn(["b", "c"]);
expect(mockFn).toBeCalledTimes(2);
expect(mockFn).toBeCalledWith("a");
expect(mockFn).toBeCalledWith(["b", "c"]);
});
3) Snapshot testing(파일 스냅샷 생성법)
스냅샷테스팅이란 어떤 기능의 예상 결과를 미리 정확히 포착해놓고 실제 결과에 비교하는 테스트이다. 테스트 대상기능의 구현이 변경되어 실제 결과와 스냅샷을 떠놓은 예상 결과와 달라질 경우 해당 테스트 케이스는 실패하게 되는데 이럴경우 새로운 스냅샷(-u옵션)을 떠서 새롭게 교체할수도있다.
import renderer from 'react-test-renderer';
import Link from '../Link';
it('renders correctly', () => {
const tree = renderer
.create(<Link page="http://www.facebook.com">Facebook</Link>)
.toJSON();
expect(tree).toMatchSnapshot();
});
//snapshot file
exports[`renders correctly 1`] = `
<a
className="normal"
href="http://www.facebook.com"
onMouseEnter={[Function]}
onMouseLeave={[Function]}
>
Facebook
</a>
`;
크게 DOM에 컴포넌트를 렌더링 해주는 render()함수와 특정 이벤트를 발생시켜주는 fireEvent객체, 그리고 DOM에서 특정영역을 선택하기위한 다양한 쿼리함수들이 존재한다.
1) render()
테스트 환경에서 해당 컴포넌트를 렌더링하여 후의 상태를 확인할수있다. 단순히 컴포넌트를 렌더하는것만이 아니라 이 함수가 호출되는순간 그 결과물에는 DOM을 선택할수있는 다양한 쿼리들과 렌더링된 DOM요소를 반환한다(=container). 일반적인 DOM노드이기때문에 container.querySelector와 같이 사용이 가능하다. 자바스크립트 객체 destructuring 문법으로 render()함수가 리턴한 객체로 부터 원하는 쿼리 함수만 담아 사용할수있다.
import React from "react";
function NotFound({ path }) {
return (
<>
<h2>Page Not Found</h2>
<p>해당 페이지({path})를 찾을 수 없습니다.</p>
<img
alt="404"
src="https://media.giphy.com/media/14uQ3cOFteDaU/giphy.gif"
/>
</>
);
}
import React from "react";
import { render } from "@testing-library/react";
import NotFound from "./NotFound";
describe("<NotFound />", () => {
it("renders header", () => {
const { getByText } = render(<NotFound path="/abc" />);
// NotFound컴포넌트를 render함수의 인자로 넘긴후 리턴객체로 부터 getByText() 함수를 얻는다.
const header = getByText("Page Not Found");
//Page Not Found 텍스트를 인자로 넘긴 후 엘리먼트를 얻는다 (h2태그가 되겠죠?)
expect(header).toBeInTheDocument();
//jest-dom의 toBeInTheDocument() matcher함수를 이용해 해당 엘리먼트가 있는지 검증한다.
});
});
2) screen()
render 함수를 호출한 결과로 쿼리함수를 사용할수도있지만 react testing library에서 제공하는 screen을 통해 쿼리 함수를 사용할수도있다. 좀 더 간편한 느낌이 드는것도 사실. (screen을 사용하기 위해서 먼저 render함수를 통해 컴포넌트를 렌더링해야한다)
import React from "react";
import { render, screen } from '@testing-library/react'
import NotFound from "./NotFound";
describe('NotFound', ()=>{
it("renders header", ()=>{
render(<NotFound path="/abc" />)
expect(screen.getByText('Page Not Found')).toBeInTheDocument();
})
})
3) 사용가능한 다양한 쿼리
앞서 말했듯이 render함수를 싱해하고 나면 그 결과물안에는 다양한 쿼리함수들이 있다.
이 쿼리함수들은 react-testing-library기반인 dom-testing-library에서 지원하는 함수들이다.
이 쿼리 함수들은 variant와 Queries의 조합으로 네이밍이 되어있는데 varient에 어떤 종류들이 있는지 알아보자.
Varient
Queries
//AlbumList.jsx
export default function AlbumList({album}){
const {title, cover, description} = album.snippet
const navigate = useNavigate();
return(
<li onClick={()=>{navigate(`/albums/${album.id}`,{state:{album:album}})}}>
<img src={cover.url} alt={title} />
<div>
<p>{title}</p>
<p>{description}</p>
</div>
</li>
)
}
//AlbumList.test.js
describe('AlbumList', ()=>{
const album = {
id:1,
snippet: {
title: 'title',
description: '1',
cover:{
url: 'http://albumCover/',
}
}
}
it('renders Album list item', ()=>{
render(
<MemoryRouter>
<AlbumList album={album}/> //AlbumList 컴포넌트를 렌더링해서 HTML 리턴
</MemoryRouter>
)
const image = screen.getByRole('img'); //HTML 역할에 대해 위 ARIA in HTML 링크 참고
//getBy+ByRole = getByRole, img역할을 지니고 있는 DOM 엘리먼트를 선택
expect(image.src).toBe(cover.url)
expect(image.alt).toBe(title)
expect(screen.getByText(title)).toBeInTheDocument()
expect(screen.getByText(description)).toBeInTheDocument()
})
})
MemoryRouter란?
테스트하고자하는 컴포넌트에 리액트 라우터 (link나 useNavigate 등)를 사용하고있으면 테스트.js에서도 그 환경을만들어줘야한다. MemoryRouter을 사용하면 환경을 만들어줄수있다.
자세한 내용은 아래 참고!
https://v5.reactrouter.com/web/guides/testing
https://reactrouter.com/en/main/router-components/memory-router
fireEvent 보다는 userEvent가 추천되고있으니 자세한 내용은 아래를 참조!
https://testing-library.com/docs/ecosystem-user-event/#typeelement-text-options
export default function InputTest(){
const [text, setText] = useState('')
const navigate = useNavigate()
const onSubmitHandler = (e)=>{
e.preventDefault();
navigate(`/albums/${text}`)
}
<div>
<form onSubmit={onSubmitHandler}>
<input
type='text'
placeholder='Search...'
value={text}
onChange={(e) => setText(e.target.value)}
/>
<button><BsSearch/></button>
</form>
</div>
}
describe('InputTest',()=>{
it('navigates to results page when the search button is clicked', ()=>{
const searchKeyword = 'testing-keyword'
render(
<MemoryRouter initialEntries={["/home"]}>
<Routes>
<Route path='/home' element={<InputTest/>}></Route>
<Route path={`/albums/${searchKeyword}`} element={<p>{searchKeyword}</p>}></Route>
</Routes>
</MemoryRouter>
)
const searchInput = screen.getByRole('textbox')
const searchButton = screen.getByRole('button')
userEvent.type(searchInput, searchKeyword)
userEvent.click(searchButton) //클릭이 되면 두번째 route 컴포넌트가 나와야함.
expect(screen.getByText(searchKeyword)).toBeInTheDocument()
})
})
1) findBy
react testing library는 비동기 테스트를 수월하게 할수있도록 다양한 함수를 제공해주는데 findBy로 시작되는 findByRole(),getByLabelTest() 가 그렇다.
const button = screen.getByRole('button', {name: 'Click Me'})
fireEvent.click(button)
await screen.findByText('Clicked once')
fireEvent.click(button)
await screen.findByText('Clicked twice')
2) waitFor
일정시간 동안 기다려야하는 경우 waitFor을 사용해서 예상결과가 통과할때까지 기다릴수있다. 또한 인자로 함수를 받기때문에 유용하게 사용할수있다.
// 콜백함수가 에러를 던지지 않을때까지 기다린다.
// 즉 mock함수가 한번이라도 불릴때까지 기다린다는 말이다.
await waitFor(() => expect(mockAPI).toHaveBeenCalledTimes(1))
// ...
test("ON button will be enabled when clicked (waitFor)", async () => {
render(<Button />);
userEvent.click(screen.getByRole("button"));
const button = await waitFor(() =>
screen.getByRole("button", {
name: /on/i
})
);
expect(button).toBeInTheDocument();
expect(button).toBeEnabled();
});
3) waitForElementToBeRemoved
DOM상에서 특정 엘리먼트가 비동기로 사라지는지 확인할때 사용할수있다.
test("OFF button will be removed when clicked", async () => {
render(<Button />);
userEvent.click(screen.getByRole("button"));
await waitForElementToBeRemoved(() =>
screen.queryByRole("button", {
name: /off/i
})
);
});
가장 자주사용되고 유용한 API들과 개념을 정리해보았다.
이외에도 asFragment나 정말 다양한 userEvent들이 있어,
시간날때마다 공식문서를 확인하면서 잘 작성하면 더욱 좋은 코드가 탄생할것이다!
또한 e2e테스트로 Cypress를 사용해서 연습했는데 꽤나 신기했고 생각보다 편리했다.
머리속에 정리가되면 작성해봐야겠다.
출처 (혹시나 누락된 출처가 있다면 꼭 알려주셔요 반영토록 하겠습니다 :))
- https://taenami.tistory.com/90
- https://www.daleseo.com/react-testing-library/
- https://velog.io/@velopert/react-testing-library
- https://velog.io/@suld2495/React-Testing-Library%EC%9D%84-%EC%82%AC%EC%9A%A9%ED%95%98%EC%97%AC-%ED%85%8C%EC%8A%A4%ED%8A%B81-%EA%B8%B0%EB%B3%B8
- https://inpa.tistory.com/entry/JEST-%F0%9F%93%9A-%EB%AA%A8%ED%82%B9-mocking-jestfn-jestspyOn?category=914656