시작하며..
프로젝트를 진행하면서 코드 리뷰를 하다보면 어떤 의도로 해당 코드를 작성했는지 직접 확인 혹은 설명을 들어야 하기에 많은 시간을 잡아먹게 된다. 또한 GitAction을 통해 CI/CD 파이프라인을 만들었다 생각했지만, 사실 CI단계에서 빌드에러만 검증하고 있었다.
이러한 점을 테스트코드를 작성하게 되면 해결할 수 있지만 '어떤 그리고 어떻게' 작성 해야하는지가 막막했었다. 이를 정리해보려고 한다. 그 중에서 단위테스트와 통합 테스트에 사용할 수 있는 내용을 정리하려고 한다.
설명 내용은 RTL (React testing-library)에 관한 것이다.
테스트코드의 장점과 작성원칙
테스트란 앱의 품질 안정성 높이기 위해 사전에 결함을 찾아내 수정하기 위한 일련의 행위다.
테스트 코드를 쓰게 되면 얻게 되는 장점은 3가지가 있다.
1. 리팩토링 작업에 도움이 된다.
2. 잘 쓰여진 테스트코드는 그 자체로 문서의 역할을 한다.
3. 좋은 설계에 대한 고민을 하게 된다.
이러한 역할을 수행하는 테스트코드는 보통 AAA(Arrange Act Assert)원칙에 의해서 작성이 된다.(Given when then)
- Arrange : 흔히 말하는 Given이며 어떤 상황인지를 말한다( ex : 어떤 컴포넌트에서, 로그인이 된 상황에서~~)
- Act : when이며 테스트할 동작을 말한다 ( ex :버튼을 눌렀을 때)
- Assert : then이며 어떤 동작이 일어나는지를 검증한다 ( ex : 값을 증가한다, 무슨 함수가 호출된다.)
// App.test.jsx
import { render, screen } from '@testing-library/react';
import App from './App';
test('renders learn react link', () => {
render(<App />);
const linkElement = screen.getByText(/learn react/i);
expect(linkElement).toBeInTheDocument();
});
테스트 코드를 써본적이 없더라도, CRA를 통해 프로젝트를 생성하면 위와 같은 파일이 생성된 것을 확인할 수 있다.
위의 코드를 해석하면 test할 상황에 대한 설명은 learn react link를 렌더하는가이고,
App을 렌더링한 상황에서( Arrange)
대소문자를 무시하고 leran react란 단어가 document에 존재할 것이다! (Assert)
이 경우에는 렌더링에 대한 검증이기에 Act가 생략이 되어있다.
그리고 이러한 검증은 항상 유저의 입장에서 앱을 이용했을 때를 고려해서 작성해야 한다.
위의 예시를 보면 알 수 있듯이 테스트 코드는 특정 요소를 렌더링 된 화면에서 찾아서 그것의 상태를 확인한다.
찾는 것을 쿼리(Queries) , 그리고 이런 것을 확인하는 것을 매쳐(matchers)라고 한다.
쿼리(Queries)

쿼리의 경우 시작하는 부분을 통해서 목적이 구별된다.
위의 표를 통해 각각의 목적을 정리하면 아래와 같다.
- getBy : 해당 요소를 얻을 때 사용함
- queryBy : 해당 요소의 존재 유무를 확인할 때 사용함 ( 없을 때, 에러 대신 null이 뜸))
- findBy : 데이터를 요구하는 비동기 작업을 테스트할 때 사용한다 retry를 통해 비동기적 요소들을 찾음
- All : 해당 쿼리문의 탐색 결과가 여러 개인 경우 사용함
즉 위의 예시에서는 learn react가 하나로 된 문자열이기에 getByText를 사용한 것이다.(getBy+ Text)
쿼리의 끝 부분은 찾을 대상의 종류를 정의하는데 사용된다.
테스트 코드는 유저의 입장에서 검증해야 한다. 따라서 우리가 특정 컴포넌트를 찾을때도 유저입장에서 찾을 수 있는 키워드를 사용하는 것이 선호된다.
이를 공식문서에서는 우선순위를 3단계를 두어 분류 한다.
1순위 : 누구나 접근 가능한 요소들
유저는 웹페이지에 접속했을 때, 클래스 네임이나 id등을 알지는 못한다. 보통 '버튼이냐','무슨 글자가 적혔나?' 등으로 구별하는데 이와 관련된 요소들이다.
- getByRole: 가장 선호되는 방식으로 대상의 역할로 구분하고, name 을 사용해서 이름을 확인할 수 있다. ex) getByRole('button', {name: /submit/i})
역할의 종류는 https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/ARIA_Techniques#roles에서 확인 가능하다. - getByLabelText: 웹사이트 양식을 탐색할 때 사용자는 라벨 텍스트를 사용하여 요소를 찾기 때문에, 이를 이용
- getByPlaceholderText: PlaceHolder로 찾기
- getByText: 양식 외부에서는 텍스트 콘텐츠가 사용자가 요소를 찾는 주요 방법.
- getByDisplayValue: 양식 요소의 현재 값은 값이 채워진 페이지를 탐색할 때 유용함.
2순위 : 시맨틱 쿼리
일반적으로 혹은 항상 유저가 보는 요소들은 아니지만 대안으로 사용 가능
- getByAltText: 이미지등의 Alt를 사용하는 요소들에 사용 가능.
- getByTitle: 기본 유저들이나 screenReader에는 읽히지 않지만 사용가능
3순위 : data-testId 부여
getByTestId : 탐색이 불가능할 때 사용가능하며, 컴퍼넌트에 data-testid를 부여해서 탐색한다.
<Grid
item
xs={6}
sm={6}
md={3}
onClick={handleClickItem}
data-testid="product-card"
>
await screen.findAllByTestId('product-card');
매쳐(matchers)
해당 요소를 찾은 다음에 우리가 원하는 결과대로 되었는지 단언할 필요가 있다.
이때 사용하는 것이 Matcher이다. (클래스를 가지게 되었는가(toHaveClass), 모양이 바꼈는가(toHaveStyle))
RTL의 매쳐의 종류는 아래와 같다.
https://github.com/testing-library/jest-dom?tab=readme-ov-file
- toBeDisabled
- toBeEnabled
- toBeEmptyDOMElement
- toBeInTheDocument
- toBeInvalid
- toBeRequired
- toBeValid
- toBeVisible
- toContainElement
- toContainHTML
- toHaveAccessibleDescription
- toHaveAccessibleErrorMessage
- toHaveAccessibleName
- toHaveAttribute
- toHaveClass
- toHaveFocus
- toHaveFormValues
- toHaveStyle
- toHaveTextContent
- toHaveValue
- toHaveDisplayValue
- toBeChecked
- toBePartiallyChecked
- toHaveRole
- toHaveErrorMessage
GitHub - testing-library/jest-dom: :owl: Custom jest matchers to test the state of the DOM
:owl: Custom jest matchers to test the state of the DOM - testing-library/jest-dom
github.com
Act 하는법
이제 쿼리문으로 대상을 찾고 매쳐로 이를 검증할 수 있는데, 우리의 테스트는 유저와의 상호 작용을 기준으로 검증해야 한다.
이때 사용하는 것이 userEvent이다. userEvent와 fireEvent는 클릭등의 이벤트를 직접 일으킬 수 있는 것은 동일하나 fireEvent와 달리 userEvent는 유저 입장에서 클릭을 일으킨다.
예를 들어 버튼을 누를 때, 유저라면 마우스를 버튼에 올리고(호버) 클릭을 한다.
하지만 fireEvent를 쓰면 이 과정없이 클릭이 된다.
따라서 테스트는 유저 입장에서 검증이 목적이기에 userEvent를 사용하는 것이 더 적합하다.

반복 작업 관리 및 테스트 독립성 유지하기
매 테스트마다 반복되는 작업들을 공통적으로 처리할 필요성이 있다.
이때 매 테스트전에 사용되는 것을 setUp, 후에 사용하는 것을 tearDown이라고 한다.
setUp :beforeEach, beforeAll
tearDown : afterEach, afterAll
아래에서는 vi라는 Vitetest의 모킹 도구로 테스트의 독립성을 유지하기 위해서 각각의 테스트가 끝난후에 모킹된 정보를 초기화 하고 있다.
afterEach(() => {
vi.clearAllMocks();
});
afterAll(() => {
vi.resetAllMocks();
});
함수 모킹하기
우리는 테스트할 때, 특정 함수들을 호출하게 되는 경우가 있다. 특히 외부 모듈에 의한 함수를 호출할 때, 인기 모듈들은 일반적으로 테스트를 거친 것이고 자체적인 테스트를 거친 후 배포된다. 이를 사용할 때, 굳이 해당 모듈에 대한 검증을 진행할 필요는 없다.
또한 특정 함수가 검증되었다면, 굳이 해당 함수를 이용하는 것이 아니라 결과가 해당 함수와 동일한 가상의 함수를 사용해도 충분하다.
이런 경우에 모킹을 하게 된다. 사용법은 아래 게시글을 참고하면 좋을 것 같다.
https://www.daleseo.com/jest-fn-spy-on/
Jest의 jest.fn(), jest.spyOn()를 이용한 함수 모킹
Engineering Blog by Dale Seo
www.daleseo.com
참고 문헌 및 강의
테스트 코드를 잘 쓴다는 개념이 막연해서 inflearn 강의를 들었고 많은 도움을 받았다. NHN테스트코드 강의를 봤었지만, 실사용에서 어려움을 겪고 구매를 했는데 만족스러웠다.
실무에 바로 적용하는 프런트엔드 테스트 - 1부. 테스트 기초: 단위・통합 테스트 | 코드 조커, 오
코드 조커, 오프 | 이 강의를 통해 전반적인 프런트엔드 테스트 종류를 파악하고, 상황에 맞는 적절한 테스트 선택을 통해 신뢰감 있는 테스트를 작성하는 방법을 배웁니다., 🎊 이벤트 🎊 1부
www.inflearn.com
https://testing-library.com/docs/react-testing-library/setup
Setup | Testing Library
React Testing Library does not require any configuration to be used. However,
testing-library.com
'프론트엔드 > 테스트' 카테고리의 다른 글
프론트엔드의 테스트 종류 (0) | 2024.04.06 |
---|---|
(React)컴파운드 패턴으로 드롭다운 컴퍼넌트 만들기-2 테스트하기 (0) | 2024.04.02 |