console.log()가 테스트 코드 아닌가요(1)

저도 그런줄 알았습니다

Intro

매주 진행하는 리액트 스터디에서 테스팅 라이브러리에 대해 발표를 준비하며, 콘솔에서 로그를 확인하는 방법 외에도 다양한 디버깅 기법이 있다는 것을 알게 되었다.

부끄럽지만 모르는 것이 죄는 아니기에 고백하자면, 지금까지 모든 디버깅은 토씨 하나를 수정하고 배포를 반복하는 방식으로 console.log에 의존해왔다.

혼자 개발하는 프로젝트에서는 모든 코드에 테스트 코드를 작성하는 것이 비용 대비 효율적이지 않을 수 있지만, 실무나 대규모 프로젝트를 위해 테스트 코드 작성은 반드시 학습해야 할 영역이다.

그리고 우리는 이 글을 통해서 결국 Jest를 이해하게 될 것이지만, Jest같은 테스팅 프레임워크가 인기있는 이유에 대해 아는 것 역시 테스팅의 이해에 많은 도움이 될 것이다. 이번 글에서는 프론트엔드 테스팅의 전반과 Jest를 활용한 테스팅을 알아보자.

본 글에서는 React Testing Libaray와 Jest만을 다룹니다

1. 테스트란?

프로그래밍에서 테스트는 로직이 의도한 계획대로 제대로 동작하는지 확인하는 과정이다.

테스트가 제공하는 기대 효과는 다음과 같다.

  • 설계한 대로 로직이 정확히 동작하는지 확인
  • 버그를 사전에 발견하고 방지
  • 오작동으로 인한 비용과 리소스 낭비 감소
  • 코드의 유지보수성과 확장성 향상

하지만 언제나 그렇듯이, 기대효과만으로 효용을 체감하기는 어렵다.

본 시리즈의 소스 코드와 함께하면 더욱 체감하기 쉬울 것이다.

2. 프론트엔드 테스트 vs 백엔드 테스트

블랙박스 테스트와 화이트박스 테스트


프론트엔드 테스트는 사용자 관점에, 백엔드 테스트는 로직 관점에 좀 더 가깝다.

1) 프론트엔드 테스트

  • 블랙박스 테스트에 가깝다. (UI/UX 중심)
  • 브라우저나 시뮬레이션된 환경에서 사용자 이벤트(클릭, 입력, 폼 제출 등)와 화면 변화를 확인
  • 주로 UI 컴포넌트, DOM 조작, 사용자 이벤트, 렌더링 상태 등을 테스트

예를 들어, 버튼을 클릭할 때마다 숫자가 1씩 증가하는 카운터 컴포넌트를 개발했다고 해보자.

프론트엔드 테스트는 “정말 버튼을 클릭하면 숫자가 증가하느냐?”라는 사용자 입장에서의 검증에 집중한다.

2) 백엔드 테스트

  • 화이트박스 테스트에 가깝다. (로직 중심)
  • API, 데이터베이스 트랜잭션, 비즈니스 규칙 등을 직접 확인
  • 어플리케이션 환경에서, 내부 로직을 세밀하게 검증

백엔드 테스트는 API가 요청을 정확히 파라미터로 받고, 올바른 응답을 주는지, DB 쿼리가 예상 대로 수행되는지를 좀 더 깊이 검증한다.

3. 테스트 피라미드

블랙박스 테스트와 화이트박스 테스트

프로그래밍 테스트의 종류는 일반적으로 테스트 피라미드 개념을 말한다. 테스트 피라미드는 아래로 갈수록 범위가 작아지고 테스트 속도가 빨라지는데, 프론트엔드에서 테스트 피라미드는 다음을 의미한다.

1) 단위 테스트 (Unit Test)

  • 피라미드의 바닥
  • 가장 작은 단위의 컴포넌트나 함수가 단독으로 예상대로 동작하는지 확인
  • JestReact Testing Library를 많이 사용
  • 빠르고 쉽게 자동화가 가능

2) 통합 테스트(Integration Test)

  • 피라미드의 중간
  • 여러 컴포넌트나 모듈의 조합이 제대로 상호작용하는지 확인
  • e.g. 페이지 상단의 Header, 상품 리스트 ProductList, 장바구니 Cart 등이 함께 동작할 때 문제 없는지 확인

3) E2E(End-to-End) 테스트

  • 피라미드의 꼭대기
  • 실제 브라우저나 브라우저 유사 환경에서 사용자 행동을 시뮬레이션
  • Cypress, Playwright 등을 활용
  • E2E 테스트가 너무 많으면 개발 프로세스가 느려질 수 있음
  • e.g. 사용자 흐름(로그인 → 상품 페이지 진입 → 결제 등)을 통합적으로 검증

보통은 ‘단위 테스트 + 통합 테스트’를 넓게 커버하고, 핵심 유저 시나리오에 대해 E2E 테스트를 작성하는 형태가 일반적이다.

4. 어설션(Assertion) 라이브러리

어설션(Assertion) 은 프로그램이 실행될 때 특정 조건이 반드시 참(true)임을 보장하기 위해 사용되는 검증 메커니즘을 의미한다.

주로 디버깅이나 개발 과정에서 오류 탐지를 목적으로 사용되며, 조건이 거짓(false)인 경우 에러를 발생시키거나 종료하게 만들어 개발자가 문제를 빠르게 인지할 수 있도록 돕는다. 그리고 이러한 어설션을 도와주는 라이브러리를 어설션 라이브러리라고 한다.

Node.js는 기본적으로 assert라는 모듈을 내장하고 있다.

const assert = require('assert');

function sum(a, b) {
  return a + b;
}

assert.equal(sum(1, 2), 3);
assert.equal(sum(2, 2), 4);
assert.equal(sum(1, 2), 4); // AssertionError [ERR_ASSERTION] [ERR_ASSERTION]: 3 == 4

assert 모듈을 사용한 테스트 코드를 한번 살펴보면 equal 메서드의 첫 파라미터인 sum(a, b) 함수가 리턴하는 값과 두 번째 파라미터인 값이 일치하는지를 어설션하고 있다. 그리고 그 값이 다른 경우에 세 번째 경우처럼 에러를 던진다. Node.js가 기본적으로 제공하는 assert는 equal외에도 notEqual, deepEqual, throws 등 다양한 메서드를 제공한다.

assert 이외에도 객체 지향적 문법을 제공하는 should.js나, 행동 주도 개발(BDD(Behavior-Driven Development, BDD)와 테스트 주도 개발(Test-Driven Development, TDD)를 함께 지원하는 chai 등의 사용성과 확장성이 뛰어난 다양한 라이브러리가 있다.

5. React Testing Library

react-testing-library(이하 RTL)는 dom-testing-library(이하 DTL)를 기반으로 설계되었으며, 이러한 DTL은 jsdom 기반으로 설계되었다.

무슨 말인지 선뜻 이해하기 어렵다. js-dom부터 역순으로 이해해보자.

1) jsdom

jsdom은 순수 JavaScript 환경에서 브라우저의 DOM과 HTML을 모방해주는 라이브러리다.

Node.js 환경에서 브라우저 API를 구현하여, 실제 브라우저 없이도 DOM 조작을 가능하게 한다.

즉, 브라우저 환경을 필요로 하는 코드를 Node.js만으로 테스트할 수 있게 도와준다.

2) dom-testing-library (DTL)

DTL은 DOM 요소를 사용자 관점에서 상호작용 할 수 있도록 도와주는 유틸리티 함수를 제공한다.

후술하겠지만, getBy..., queryBy...와 같은 쿼리 함수들을 통해 코드의 구현에 의존하지 않고 실제 사용자가 보는 방식대로 요소를 선택할 수 있게 해준다.

정리하면 jsdom과 같은 모방된 DOM 환경에서 동작하지만, 테스트 코드를 보다 사용자 관점에서 직관적으로 작성할 수 있게 도와준다.

3) react-testing-library (RTL)

마지막으로 프레임워크에 독립적인 DTL과 달리, RTL은 React 전용으로 만들어졌다.

render, screen과 같은 React 특화 기능을 통해 컴포넌트를 쉽게 렌더링하고, React의 훅이나 컴포넌트의 라이프사이클을 고려한 테스트를 지원한다

즉, DTL은 DOM 테스트를 위한 범용 유틸리티 모음이고, RTL은 이 DTL을 활용해 React 컴포넌트를 테스트하기 위한 특별한 도구이다.

4) RTL의 주요 쿼리

// 텍스트로 요소 찾기
getByText('Submit');
queryByText('Error');
findByText('Loading...');

// 역할로 요소 찾기
getByRole('button');
queryByRole('alert');
findByRole('progressbar');

// 레이블로 요소 찾기
getByLabelText('Username');
queryByLabelText('Password');
findByLabelText('Email');

// 테스트 ID로 요소 찾기
getByTestId('submit-button');
queryByTestId('error-message');
findByTestId('loading-spinner');

getBy...

  • 인수의 조건에 맞는 요소를 반환한다
  • 인수의 조건에 맞는 요소가 없거나, 두 개 이상이면 에러를 발생시킨다.
  • 반환하고자 하는 요소가 두 개 이상이면 getAllBy... 를 사용해야한다.
// Component.test.js
import { render, screen } from '@testing-library/react';
import Component from './Component';

test('Component renders a welcome message', () => {
    render(<Component />);
    // getByText를 통해 'Welcome' 텍스트를 가진 Element를 찾는다.
    const welcomeElement = screen.getByText('Welcome');
    // 'Welcome' 텍스트를 가진 요소가 반드시 있어야 한다.
    expect(welcomeElement).toBeInTheDocument();
});

findBy...

  • getBy와 유사하나 Promise를 반환한다.
  • 비동기 액션 이후에 원하는 요소를 찾을 때 사용한다.
  • 마찬가지로 반환하고자 하는 요소가 두 개 이상이면 findAllBy... 를 사용해야한다.
// AsyncComponent.test.js
import { render, screen } from '@testing-library/react';
import AsyncComponent from './AsyncComponent';

test('AsyncComponent eventually displays data', async () => {
    render(<AsyncComponent />);
    // 비동기로 나타나는 'Data Loaded' 텍스트를 기다린다.
    const dataElement = await screen.findByText('Data Loaded');
    // 'Data Loaded' 텍스트를 가진 요소가 반드시 있어야 한다.
    expect(dataElement).toBeInTheDocument();
});

queryBy...

  • 조건에 맞는 요소를 반환하지만 다른 함수와는 달리, 요소가 없어도 에러가 아닌 null을 반환한다.
  • 마찬가지로 반환하고자 하는 요소가 두 개 이상이면 queryAllBy... 를 사용해야한다.
// Component.test.js
import { render, screen } from '@testing-library/react';
import Component from './Component';

test('Component does not render an error message', () => {
    render(<Component />);
    // 'Error' 텍스트를 가진 요소를 찾는다.
    const errorElement = screen.queryByText('Error');
    // 'Error' 텍스트를 가진 요소가 없음을 확인 (null이 반환됨)
    expect(errorElement).toBeNull();
});

다음 글에서 Jest를 활용한 구체적인 테스트 코드와 함께 Jest 사용 방법에 대해 알아보자.

Reference