좋은 리액트 코드 작성을 위한 환경 구축하기 (2)

keemsebeen·2024년 7월 8일

모던 리액트 Deep Dive

목록 보기
14/18

8.2 리액트 팀이 권장하는 리액트 테스트 라이브러리

프론트엔드에서의 테스트는 사용자가 프로그램에서 수행할 주요 비즈니스 로직이나 모든 경우의 수를 고려해야 하며, 이 과정에서 굳이 프론트엔드 코드를 알 필요는 없다. 즉 블랙박스 형태로 테스트가 이뤄지며, 코드가 어떻게 됐든 상관없이 의도한 대로 작동하는지 확인하는데 초점이 맞춰져 있다.

React Testing Library란?

React Testing Library는 Dom Testing Library를 기반으로 만들어졌다.
Dom Testing Library는 jsdom을 기반으로 하고 있는데, jsdom이란 순수하게 자바스크립트로 작성된 라이브러리로, HTML이 없는 자바스크립트만 존재하는 환경에서 HTML과 DOM을 사용할 수 있도록 해주는 라이브러리다.

해당 라이브러리를 기반으로 동일한 원리로 리액트 기반 환경에서 리액트 컴포넌트를 테스팅할 수 있는 라이브러리가 바로 리액트 테스팅 라이브러리다.

자바스크립트 테스트의 기초

  1. 테스트할 함수나 모듈을 선정한다.
  2. 함수나 모듈이 반환하길 기대하는 값을 적는다.
  3. 함수나 모듈의 실제 반환 값을 적는다.
  4. 3번의 기대에 따라 2번의 결과가 일치하는지 확인한다.
  5. 기대하는 결과를 반환한다면 테스트는 성공이며, 만약 기대와 다른 결과를 반환하면 에러를 던진다.

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) // 에러
})

리액트 컴포넌트 테스트코드 작성하기

기본적으로 리액트에서 컴포넌트 테스트는 다음과 같은 순서로 진행된다.

  1. 컴포넌트를 렌더링한다.
  2. 필요하다면 컴포넌트에서 특정 액션을 수행한다.
  3. 컴포넌트 렌더링과 2번의 액션을 통해 기대하는 결과와 실제 결과를 비교한다.

프로젝트 생성

App.test.tsx가 App.tsx에서 테스트하는 내용은 다음과 같이 요약할 수 있다.

  1. <App /> 을 렌더링한다.
  2. 렌더링하는 컴포넌트 내부에서 “learn react”라는 문자열을 가진 DOM요소를 찾는다.
  3. expoet(linkElement).toBeInTheDocument()라는 어설션을 활용해 2번에서 찾은 요소가 documnet 내부에 있는지 확인한다.

HTML 요소가 있는 지 여부를 확인하는 방법은 다음과 같다.

  • getBy … : 인수의 조건에 맞는 요소를 반환한다. 해당요소가 없거나 두개 이상이면 에러를 발생시킨다.
  • findBy … : getBy…와 거의 유사하나, 차이점은 Promise를 반환한다는 것이다.
  • queryBy … : 인수의 조건에 맞는 요소를 반환하는 대신, 찾지 못하면 null을 반환한다.

정적 컴포넌트

예시

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;')
	})
})
  • beforeEach : 각 테스트(it)를 수행하기 전에 실행하는 함수다. StaticComponent를 렌더링한다.
  • describe : 비슷한 속성을 가진 테스트를 하나의 그룹으로 묶는 역할을 한다.
  • it : test와 완전히 동일하며, test의 축약어이다.
  • testId : 리액트 테스팅 라이브러리의 예약어로, get 등의 선택자로 선택하기 어렵거나 곤란한 요소를 선택하기 위해 사용할 수 있다.

동적 컴포넌트

예시

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)
	})
})
  • setup 함수 : 내부에서 컴포넌트를 렌더링하고, 또 테스트에 필요한 button과 input을 반환한다.
  • userEvent.type : 사용자가 타이핑하는 것을 흉내 내는 메서드다.
  • jset.spyOn(window,’alert’).mockImplementataion()
    • spyOn : 어떠한 메소드를 오염시키지 않고 실행이 됐는지, 어떤 인수로 실행됐는지 등 실행과 관련된 정보만 얻고 싶을 때 사용한다.
    • mockImplementataion : 모킹 구현을 도와준다.

비동기 이벤트가 발생하는 컴포넌트

서버 응답에서 오류가 난 경우나 데이터를 불러오는 등 여러가지 상황을 위해 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))
		}
	}),
)
  • setupServer는 MSW에서 제공하는 메소드로, 이름 그대로 서버를 만드는 역할을 한다.
beforeAll(() => server.listen())
afterEach(() => server.resetHandlers())
afterAll(() => server.close())
  • resetHandlers는 앞에 선언한 setupServer의 기본 설정으로 되돌리는 역할을 한다. 서버에서 실패가 발생할 경우 ctx.status(503)과 같은 형태로 변경할 것이다. 이를 리셋하지 않으면 계속해서 실패하는 코드로 남아있을 것이므로 테스트 실행마다 resetHandlers를 통해 setupServer로 초기화했던 초깃값을 유지하는 것이다.

사용자 정의 훅 테스트하기

useEffectDebugger

  1. 최초 컴포넌트 렌더링 시에는 호출하지 않는다.
  2. 이전 props를 useRef에 저장해 두고, 새로운 props를 넘겨받을 때마다 이전 props와 비교해 무엇이 렌더링을 발생시켰는지 확인한다.
  3. 이전 props와 신규 props의 비교는 리액트의 원리와 동일하게 Object.is를 활용해 얕은 비교를 수행한다.

다만 이는 props가 변경되는 것만 확인이 가능하다. 부모 컴포넌트가 리렌더링되는 경우 useEffectDebugger로 확인할 수 없다.

테스트를 작성하기에 앞서 고려해야 할 점

프론트엔드 코드는 사용자의 입력이 매우 자유롭기 때문에 모든 상황을 커버해 테스트를 작성하기란 불가능하다. 따라서 테스트 코드를 작성하기 전에 생각해봐야 할 최우선 과제는 애플리케이션에서 가장 취약하거나 중요한 부분을 파악하는 것이다. 가장 핵심이 되는 부분부터 테스트코드를 하나씩 작성해 나가는 것이 중요하다.

테스트 코드는 소프트웨어의 코드를 100% 커버하기 위해, 혹은 테스트 코드가 모두 그린 사인을 보기 위해 작성하는 것이 아니다. 개발자가 간순 코드 작성만으로 쉽게 이룰 수 없는 목표인 소프트웨어 품질에 대한 확신을 얻기 위해 작성하는 것임을 기억하자!

profile
프론트엔드 개발자 김세빈입니다. 👩🏻‍💻

0개의 댓글