기타

[기타] (퍼온글) 프론트엔드 테스트전략

youngble 2022. 3. 16. 23:34

Introduction

프론트엔드 개발 환경이 최근 몇 년간 급격하게 발전함에 따라 프론트엔드 환경에서 요구하는 애플리케이션 수준이 복잡해지기 시작했고, 이에 따라 테스트 환경 또한 고도화되면서 새로운 테스트 방법론과 도구들이 등장하게 되었습니다. 이번 포스팅에서는 프론트엔드를 테스트하기 위한 방법들과 효율적으로 테스트 할 수 있는 전략을 세우는 방법에 대해 알아보도록 하겠습니다.

소프트웨어 관점의 테스트

위키피디아에서는 소프트웨어 테스트를 다음과 같이 정의하고 있습니다.

Test techniques include the process of executing a program or application with the intent of finding failures, and verifying that the software product is fit for use.

직역하면 “프로그램을 실행하여 오류와 결함을 검출하고 애플리케이션이 요구사항에 맞게 동작하는지 검증하는 절차”를 말합니다. 프론트엔드 개발자가 디자인을 보고 마크업을 한 뒤, 브라우저에서 렌더링 된 결과를 확인하는 일련의 과정도 테스트이며, 새로운 기능을 추가한 뒤 제대로 동작하는지 브라우저에서 확인하는 것과 기존 코드를 리팩토링한 후 영향을 받는 다른 모듈이 기존과 동일하게 동작하는지 확인하는 것 또한 테스트에 해당합니다. 그렇습니다, 사실 개발자들은 테스트와 개발을 동시에 하고, 어쩌면 테스트를 하는데 더 많은 시간을 사용합니다.

테스트의 목적과 자동화

위와 같이 테스트를 하는 목적은 발생 가능한 결함을 예방하고 애플리케이션이 요구사항을 충족시키는지 확인하는 데에 있습니다. 또한 테스트를 하면 개발 과정에서 생기는 변경 사항들로 인해 새로운 결함이 유입되지 않았는지 확인할 수 있기 때문에 개발자는 애플리케이션 품질 수준에 대한 자신감을 얻을 수 있고 더 적극적으로 리팩토링 및 코드 개선을 할 수 있어서 궁극적으로 코드 품질이 향상되게 됩니다.

좋은 소식은 오늘날 여러 도구의 도움으로 (프론트엔드) 개발자가 수행해왔던 반복된 테스트를 자동화할 수 있다는 것입니다. 그리하여 사람이 수행하는 것보다 훨씬 빠른 속도로 더 신뢰할 수 있는 테스트를 수행할 수 있습니다. 물론 모든 영역에서 자동화된 테스트를 도입할 수 있는 것은 아닙니다. 사람의 감각을 필요로 하는 곳과 실제 운영 환경에서 발생하는 모든 상황을 테스트하는 것은 여전히 어렵습니다. 그럼에도 불구하고 테스트를 자동화하는 것은 QA와의 불필요한 커뮤니케이션 비용을 절감하고 개발자가 자신 있게 코드를 개선할 수 있단 점에서 큰 이점이 있습니다.

테스트의 유형

테스트는 테스트를 바라보는 관점에 따라 분리할 수 있는 여러 기준이 있는데, 오늘날 테스트의 유형을 구분 짓는 많은 가이드에 의하면 테스트는 다음과 같이 정적(Static) 테스트, 단위(Unit) 테스트, 통합(Integration) 테스트, E2E(End to End) 테스트로 구분할 수 있습니다.

Static Test

정적 테스트는 코드를 실행시키지 않고 테스트를 하는 것을 말합니다. 정적 테스트를 하면 Type 에러나 Reference 에러와 같은 개발자의 실수로 인해 발생하는 에러를 미연에 방지할 수 있습니다. 또한 많은 개발자의 노하우에 의해 만들어진 규칙에 따라 코드를 분석하는 것은 더 좋은 코드를 작성할 수 있는 습관을 기르도록 도와줍니다.

프론트엔드의 경우 ESLint 를 활용하여 사용하지 않는 변수를 찾거나 Typescript 로 함수의 인자로 받는 파라미터의 타입 검사를 하는 것이 정적 테스트에 포함됩니다.

Unit Test

단위 테스트는 각 모듈을 단독 실행 환경에서 독립적으로 테스트하는 것을 말합니다. 단위 테스트는 의존성이 있는 코드와 함께 테스트하는 Sociable 테스트와 모듈에 의해 실행되는 코드를 테스트 더블로 대체하는 Solitary 테스트가 있습니다.

Note: 테스트 더블이란 테스트를 위해 사용되는 모의 객체/코드를 말합니다. 테스트 더블에 대한 자세한 용어와 의미는 마틴 파울러의 블로그 게시글을 참고하세요.

Unit Test: sociable test VS solitary test(출처: https://martinfowler.com/bliki/UnitTest.html)

프론트엔드에서 단위 테스트는 특정 컴포넌트를 렌더링해서 깨지지 않는지 확인하는 것을 예로 들 수 있습니다. Sociable 테스트는 자식(child) 컴포넌트까지 포함해서 렌더링하는 것이고, Solitary 테스트는 자식 컴포넌트를 모킹해서 렌더링 하는 것으로 볼 수 있습니다.

Integration test

통합 테스트는 두 개 이상의 모듈이 실제로 연결된 상태를 테스트하는 것을 말합니다. 그리하여 모듈 간의 연결에서 발생하는 에러를 검증할 수 있으며, 단위테스트보다 비교적 넓은 범위에서 테스트하기 때문에 리팩토링에 쉽게 깨지지 않는 장점이 있습니다.

통합 테스트는 단위 테스트와 마찬가지로 테스트 더블의 사용 여부에 따라 broad test와 narrow test로 구분되는데, broad test는 의존성이 있는 모든 모듈이 연결된 상태를 테스트하는 것을 말하고 narrow test는 연결된 모듈을 테스트 더블로 대체하여 테스트하는 것을 말합니다.

Integration Test: broad test VS narrow test(출처: https://martinfowler.com/bliki/IntegrationTest.html)

프론트엔드에서 통합 테스트는 UI와 API 간의 상호작용이 올바르게 일어나는지, 또는 state에 따른 UI의 변경이 올바르게 동작하는지를 확인하는 것으로 생각해볼 수 있습니다. 그리하여 실제 API를 호출하여 broad test를 하거나 API client를 모킹(mocking) 하거나 가상 API 서버를 이용함으로써 narrow test를 수행할 수 있습니다.

E2E Test

E2E 테스트는 실제 사용자의 입장 및 환경에서 테스트하는 것을 말합니다. 프론트엔드에서 E2E 테스트는 실제 브라우저를 실행해서 테스트하는 것을 말하며, 커버리지가 높고 실제 상황에서 발생할 수 있는 에러를 검출할 수 있단 장점이 있습니다. 또한 브라우저를 띄우기 때문에 Web API를 활용할 수 있고 테스트 코드가 내부 구조에 영향을 받지 않기 때문에 코드의 변경에도 비교적 잘 깨지지 않는 장점이 있습니다.

그러나 브라우저를 띄우기 때문에 단위, 통합 테스트보다 실행 속도가 느리고 테스트의 실행 환경에서 발생하는 문제들(네트워크 에러 등)로 인해 테스트가 실패할 수 있어서 E2E 테스트의 결과를 신뢰하기 어려운 단점도 존재합니다.

프론트엔드 관점의 테스트

지금까지 소프트웨어 공학적인 관점에서 테스트에 대해서 알아보았습니다. 그러면 프론트엔드는 어떻게 테스트 할 수 있을까요? 이 질문에 대한 답변이 막연하게 느껴질지 몰라도 이미 위에서 ‘개발자들은 테스트와 개발을 동시에 한다’라고 언급했던 것처럼, 프론트엔드 개발자의 역할을 잘 생각해보면 테스트의 대상을 좀 더 명확하게 구분할 수 있습니다.

테스트 대상

프론트엔드에서 테스트의 대상은 다음과 같이 크게 3가지(시각적 요소, 사용자 이벤트 처리, API 통신)를 생각해 볼 수 있습니다.

시각적 요소

프론트엔드 개발자는 디자이너로부터 전달받은 디자인을 보고 HTML로 마크업을 한 뒤 CSS로 컴포넌트에 스타일을 입히는 작업을 수행합니다. 일반적으로 HTML 구조가 의도한 대로 나타나는지를 테스트하기 위해 스냅샷 테스트를 수행하고, HTML에 CSS를 더해 컴포넌트가 실제로 브라우저에서 렌더링 되는 모습이 의도한 대로 나타나는지를 테스트하기 위해 시각적 회귀 테스트(Visual Regression Test)를 수행합니다. 스냅샷 테스트에 사용되는 도구로 Jest가 대표적이고, 시각적 회귀 테스트에 사용되는 도구로는 대표적으로 Storybook에서 만든 Chromatic이 존재합니다.

Visual Regression Test(출처: https://www.chromatic.com/)

사용자 이벤트 처리

프론트엔드 개발자는 사용자의 마우스, 키보드 등의 입력 이벤트를 적절한 이벤트 핸들러로 처리하는 작업도 담당합니다. 이를 테스트하기 위해 자바스크립트 API 또는 React와 같은 라이브러리에서 제공하는 테스트 유틸리티를 활용하여 이벤트를 시뮬레이션하거나 E2E 테스트를 통해서 실제로 브라우저상에서 이벤트를 발생시킬 수 있습니다. Node.js 환경에서 테스트하는 경우 React 팀이 권장하는 테스트 도구 testing-library에서 제공하는 패키지를 활용하면 사용자 이벤트를 보다 효율적으로 시뮬레이션 할 수 있습니다.

API 서버 통신

프론트엔드 개발자는 브라우저 API 또는 라이브러리를 활용하여 API 서버와 통신하고 애플리케이션 상태를 동기화하는 작업도 수행합니다. API 서버와의 통신을 테스트하는 방법은 여러 가지가 있는데, 첫 번째로 실제 API 서버를 이용하는 방법이 있으며 이 방법은 E2E 테스트에서 주로 사용됩니다. 그리고 테스트 API 서버를 구축하거나 API client를 모킹하는 방법이 존재합니다. 이렇게 API client를 모킹하면 API 요청에 대한 응답을 원하는 대로 수정할 수 있기 때문에 다양한 상황을 테스트할 수 있다는 장점이 있습니다. Jest를 활용하면 모듈을 간단한 방법으로 모킹할 수 있기 때문에 testing-library와 마찬가지로 Jest를 테스트 도구로 활용할 것을 React 팀에서 권장하고 있습니다.

테스트 환경

위에서 소개한 3가지 대상을 테스트하는 방법을 소개해 드리기 전에, 프론트엔드 테스트 환경에는 어떤 것들이 있고 각 환경이 어떻게 다른지 이해하는 것이 필요합니다. 그 이유는 테스트 환경에 따른 트레이드 오프가 존재하며 테스트 환경별로 다른 도구들이 사용되기 때문입니다.

프론트엔드 환경은 테스트 코드가 어디에서 실행되는지에 따라서 브라우저 환경과 Node.js 환경으로 구분됩니다.

브라우저 환경

브라우저 환경에서 테스트 코드를 실행하면 모든 Web API에 접근할 수 있단 것이 가장 큰 장점입니다. 또한 서로 다른 브라우저에서 테스트 할 수 있기 때문에 브라우저 호환성 및 기기 호환성 테스트도 진행할 수 있습니다.

그러나 브라우저의 프로세스가 Node.js 프로세스보다 무겁기 때문에 실행 속도가 느리다는 단점이 있습니다. 또한 브라우저를 실행해야 하므로 대부분의 경우 브라우저 런처를 별도로 설치해야 한다는 제약이 있습니다. 그래서 속도 문제를 해결하기 위해 UI 인터페이스를 제외한 Headless 브라우저를 사용하고 개발이 완료되어 배포할 때 CI와 연동해서 테스트하는 방식이 권장되고 있습니다.

Node.js 환경

테스트 코드를 Node.js 환경에서 실행하는 것은 브라우저 환경보다 속도가 빠르다는 장점이 있습니다. 그러나 Node.js 환경은 브라우저에서 제공하는 Web API, DOM 접근 API를 사용할 수 없다는 단점이 존재합니다. 이러한 문제를 극복하기 위해 Jest와 같은 테스트 도구들은 jsdom처럼 DOM을 가상으로 구현하는 라이브러리를 활용하고 있는데, 여전히 페이지 내비게이션이나 레이아웃과 같은 것들은 테스트할 수 없다는 제약이 있습니다. 당연히 크로스 브라우징과 같은 다양한 환경에서의 테스트도 할 수 없습니다.

Trade off

React 공식 문서에서는 브라우저 환경과 Node.js 환경에서 테스트하는 것에 대한 트레이드 오프가 존재한다고 설명하고 있습니다. 위 두 가지 환경에서 테스트하는 것은 테스트 실행 속도와 실제 환경에서 테스트 함으로써 얻을 수 있는 이점 두 가지 측면에서 서로 대비되기 때문입니다. 그렇다면 적절한 테스트 환경을 선택하기 위해 어떻게 기준을 정하면 좋을까요?

TOAST UI 팀은 크로스 브라우징 또는 렌더링, 내비게이션 등 브라우저의 실제 동작을 테스트해야 하는 경우 브라우저 환경에서 테스트하고, 그 외의 경우 Node.js 환경에서 테스트하는 것을 권장하고 있습니다(참고).

그 이유는 오늘날 대부분 브라우저들의 구현 차이가 거의 없어졌고, babel로 최신 JS 문법을 트랜스파일링 함으로써 오래된 브라우저 지원도 가능하며 jsdom 으로 DOM을 구현한 뒤 React와 프레임워크에서 제공하는 API를 활용하면 DOM 조작도 가능하기 때문입니다. 그러므로 현재 프론트엔드 팀의 상황과 제품의 특징에 맞는 적절한 테스트 환경을 선택하는 것이 필요합니다.

Testing Tools

지금부터 프론트엔드 환경별로 활용할 수 있는 도구들에는 어떤 것들이 있는지 알아봅시다.

브라우저 환경

브라우저 환경에서 사용할 수 있는 여러 테스트 도구들이 존재하는데, 여기서는 Testcafe 에 대해 소개해 드리겠습니다.

Testcafe

Testcafe는 크로스 브라우징, 크로스 플랫폼 환경에서 테스트하는 데에 특화된 E2E 테스트 도구입니다. 일반적인 E2E 테스트(Web Driver를 사용하여 브라우저-서버 사이에서 요청을 주고받으며 수행하는 테스트) 도구와는 달리, Web Driver를 사용하지 않으며 브라우저-서버 사이에 URL-rewriting 프록시를 두고 사용자 이벤트를 모방하는 드라이버 스크립트를 테스트하고자 하는 페이지에 주입 함으로써 테스트코드를 실행합니다.

Testcafe 동작 원리(출처: https://testautomationu.applitools.com/testcafe-tutorial/chapter1.html)

일단 Testcafe 를 실행시키면, 프록시 서버가 실행되고, 자동화된 스크립트를 페이지에 주입하고 페이지에 포함된 모든 URL이 프록시를 가리키도록 변경됩니다. 그리하여 브라우저는 프록시에 의해 변경된 URL을 참조하게 되고, 마치 기존의 URL을 바라보고 있는 것과 동일하게 작동할 수 있습니다. 이러한 이유로 Testcafe 로 테스트를 실행시키면 브라우저 주소가 실제 접속하고자 하는 페이지 주소와는 다르게 나타납니다(위 그림에서 브라우저 주소창이 https://example.com 이 아닌 http://10.0.71.1 을 나타내고 있습니다).

Testcafe 는 이러한 구조적 특징으로 인해 다른 E2E 테스트 도구보다 다양한 장점들을 갖고 있습니다. 우선 Web Driver를 구동하는데 필요한 SDK나 라이브러리들이 필요가 없습니다. Node.js 와 브라우저만 있으면 테스트 코드를 실행할 수 있습니다. 또한 Testcafe 는 브라우저 API를 모킹하여 주입해주기 때문에 모던 브라우저를 비롯하여 IE11도 지원하는 뛰어난 브라우저 호환성을 자랑하며 윈도우, 맥, 리눅스, 안드로이드 그리고 iOS 환경을 모두 지원합니다. 추가적으로 built-in wait 메커니즘이 존재하여 매뉴얼 하게 wait() 하는 테스트 코드를 작성하지 않아도 됩니다.

그리하여 브라우저 호환성이 좋은 Testcafe 는 좋은 E2E 테스트 도구의 선택지가 될 수 있습니다.

Node.js 환경

Node.js 환경에서 실행할 수 있는 여러 자바스크립트 테스트 도구들이 존재하는데, 여기서는 React 공식 문서에서 권장하는 테스트 도구인 Jest와 testing-library에 대해 소개해 드리겠습니다.

Jest

기존에는 자바스크립트 테스트 도구들이 테스트를 실행하는 환경을 제공하는 테스트 러너와 테스트에 필요한 유틸리티 함수를 제공해주는 테스트 프레임워크, assertion 라이브러리와 모의 객체를 만드는 데 사용하는 테스트 더블 라이브러리 등으로 구분되었지만, 오늘날에는 Jest가 이들을 하나로 통합된 형태로 제공해주고 있어서 간편하게 테스트 환경을 구축하고 테스트 코드를 작성할 수 있습니다.

Jest 는 페이스북에서 개발한 자바스크립트 테스팅 프레임워크입니다. Jest를 활용하면 프론트엔드 뿐만 아니라 Node.js 환경에 구축된 백엔드 애플리케이션도 테스트할 수 있습니다. Jest는 단언(assertion)뿐만 아니라 모킹, 스냅샷 테스팅, 코드 커버리지 등 다양한 API를 제공해주고 있기 때문에 개발자가 여러 라이브러리를 학습할 필요를 줄여줍니다.

Testing library

Testing-library는 이름 그대로 UI를 사용자 관점에서 테스트할 수 있도록 도와주는 라이브러리입니다. testing-library는 등장한 지 비교적 얼마 되지 않은 라이브러리로 2018년에 처음 등장해서 지금까지 개발되고 있는 테스팅 도구들의 집합체입니다. 매년 열리는 자바스크립트 생태계 설문조사 행사인 State of JS의 2020년 설문 조사 결과에 따르면 testing-library가 만족도 기준에서 다른 테스트 라이브러리를 제치고 1위를 차지할 정도로 프론트엔드 테스트 환경 발전에 많은 기여를 하고 있습니다.

2020 테스트도구의 만족도 기준 순위(출처: https://2020.stateofjs.com/ko-KR/technologies/testing/)

Testing-library는 컴포넌트의 내부 상태 변경과 같은 상세한 구현(implementation details)을 테스트하는 기존의 프론트엔드 테스트의 문제점을 개선하기 위해 등장했습니다. 이렇게 구현을 테스트하는 것은 리팩토링, 즉 기능이 아닌 구현을 수정할 때 테스트를 깨지게 만들면서 개발 속도를 저하시키고 생산성을 떨어뜨리는 문제가 있었습니다. 이러한 문제를 해결하기 위해 Testing-library 는 다음과 같은 슬로건을 내세우고 있습니다.

The more your tests resemble the way your software is used, the more confidence they can give you.

직역하면, 사용자가 소프트웨어를 사용하는 것과 비슷하게 테스트를 하는 것이 개발자에게 더 많은 자신감을 준다는 것입니다. 즉 사용자는 컴포넌트 내부의 상태가 어떻게 바뀌고, 컴포넌트 내부 메서드가 어떻게 호출되는지에는 전혀 관심이 없기 때문에, 테스트 코드를 작성할 때도 이에 대해선 테스트하지 말고 사용자가 관심 있어 하는 곳을 테스트하자는 뜻입니다. 이러한 접근 방식은 기능이 아닌 내부 구현을 수정할 때 테스트를 깨지지 않게 함으로써 개발자가 더 자신감을 갖고 리팩토링 할 수 있도록 도와줍니다.

Recap

이번 포스팅에서는 프론트엔드에서의 테스트 대상, 환경 및 도구들을 간략하게 소개해 드렸습니다. 본 포스팅에서 소개한 내용을 정리하면 다음과 같습니다.

  • 자동화된 테스트의 목적은 발생 가능한 결함을 예방하고 애플리케이션이 요구사항을 충족시키는지 확인하는 데에 있습니다
  • 자동화된 테스트의 장점은 개발 과정에서 새로운 결함이 유입되지 않았는지 확인함으로써 적극적으로 리팩토링을 할 수 있게 되고 궁극적으로 코드 품질이 향상되는 것입니다
  • 프론트엔드에서 테스트의 대상은 크게 시각적 요소, 사용자 이벤트 처리, 그리고 API 통신이 있습니다
  • 프론트엔드에서 테스트 환경은 브라우저 및 Node.js 환경으로 구분되고, 각 환경은 실행 속도 및 실제 환경이란 두 가지 측면에서 트레이드 오프가 존재합니다
  • 브라우저 환경에서 Testcafe 를 활용하면 효과적으로 E2E 테스트 및 크로스 브라우징 테스트를 수행할 수 있습니다
  • React 팀이 권장하는 도구인 Jest, testing-library를 활용하면 Node.js 환경에서 효과적으로 기능적 테스트를 수행할 수 있습니다

 

2편

@testing-library/react Github repository introduction

지난 포스팅에서는 프론트엔드 테스트 환경, 테스트 대상과 테스트 도구들에는 어떤 것들이 있는지 알아보았습니다. 이번 포스팅에서는 Node.js환경에서 Jest, Testing-library를 활용해서 어떻게 프론트엔드 테스트 코드를 작성할 수 있는지 알아보겠습니다.

Note: 이 포스팅에서는 Jest를 테스트 러너로 사용합니다. Jest 설치 및 설정에 대해서는 다루지 않으니 이와 관련해서는 공식 도큐먼트를 참고해 주세요.

Testing React — basic

Testing-library를 활용하기에 앞서 React 공식 문서에서 제안하는 테스트 방법을 먼저 살펴보겠습니다.

Setup/Teardown

먼저 테스트 하고자 하는 컴포넌트를 렌더링하기 전에, 다음과 같이 컴포넌트를 마운트할 루트 컨테이너(container) DOM element를 생성해야 합니다. 또 각 테스트가 종료되면 해당 컨테이너를 초기화하여 다음 테스트에 영향을 미치지 않도록 해야 합니다.

 

Rendering

React는 컴포넌트 렌더링을 비롯한 유저 이벤트, 데이터 fetch 등의 UI 상호작용을 하나의 ‘단위’로 보면서 이와 관련된 변경사항들이 단언(assertion)을 하기 전에 모두 처리되어 DOM에 적용될 수 있도록 도와주는 헬퍼 함수인 act() 메서드를 제공하고 있습니다.

그리하여 다음과 같이 컴포넌트를 렌더링하는 로직을 act()로 래핑한 뒤 assertion을 수행하여 테스트를 작성할 수 있습니다.

act(() => {
  // 컴포넌트를 렌더링 합니다.
});
// act 이후에 단언을 수행합니다.

이해를 돕기 위해 다음과 같이 간단한 Counter 컴포넌트가 있다고 생각해봅시다.

 

이 카운터 컴포넌트를 테스트하기 위해 다음과 같이 테스트 코드를 작성할 수 있습니다. 먼저 setup/teardown 로직을 작성하고 컴포넌트를 렌더링하는 로직을 act() 메서드로 감싸서 렌더링이 완료된 뒤에 텍스트나 버튼이 DOM 트리에 존재하는지 assertion 할 수 있습니다.

 

위 테스트 코드를 실행하면 PASS 하는 것을 확인할 수 있습니다. 그러나 위 테스트 코드는 잘 짜인 테스트 코드라고 말하기는 어렵습니다. 그 이유는 먼저 setup/teardown 하는 보일러 플레이트가 존재하기 때문입니다. 지금은 단 하나의 컴포넌트를 테스트하기 때문에 문제 되지 않지만, 여러 컴포넌트를 테스트하게 된다면 각 테스트 코드마다 setup/teardown 로직이 중복해서 나타나게 됩니다. DRY 원칙에 위배되지 않기 위해선 개선되어야 할 부분입니다.

또 다른 이유로 현재 DOM element를 찾기 위해 .querySelector와 같은 DOM API를 활용하고 있는데, 이러한 방법은 실제로 애플리케이션 사용자가 애플리케이션을 사용하는 방법과는 다소 거리가 있습니다. 나아가 개발자가 볼 때 가독성도 떨어져서 테스트 코드를 작성한 개발자의 의도를 명확하게 파악하기 힘듭니다.

개발자가 자동화된 테스트코드를 작성하는 목적은 애플리케이션이 예상하는 대로 동작한다는 것에 대한 신뢰를 얻기 위함이라고 이전 포스팅에서 말씀드렸습니다. 테스트 코드가 실제 사용자에 의해 애플리케이션이 사용되는 방식과 닮을수록 개발자는 더 많은 신뢰를 얻을 수 있습니다. 그러면 위와 같이 작성된 테스트 코드를 어떻게 개선할 수 있을까요?

Testing React — with @Testing-library/dom

이 문제를 해결하기 위해 Testing-library가 만들어졌습니다. Testing-library는 마치 사용자가 실제로 애플리케이션을 사용하는 것과 같이 테스트 코드를 작성할 수 있도록 여러 유틸리티를 제공해줍니다. 나아가 특정 라이브러리나 프레임워크에 종속되지 않고 테스트 할 수 있도록 @testing-library/dom 패키지를 제공하고, React, Vue, Angular 등 여러 라이브러리나 프레임워크에 맞게 wrapping 한 패키지도 제공하고 있습니다.

먼저 위에서 작성한 Counter 컴포넌트 테스트 코드를 @testing-library/dom을 활용하여 다음과 같이 작성할 수 있습니다.

 

Note: Testing library는 페이지에서 특정 DOM element를 찾는 방법을 제공하며 이를 ‘쿼리(Queries)’라고 합니다. 쿼리의 종류는 element를 찾지 못 하였을 때와 retry 여부에 따라 크게 3가지(getBy..., queryBy..., findBy...)로 나누어지는데, 쿼리에 대한 자세한 내용은 공식 문서를 참고하세요.

위 코드에서와같이 getQueriesForElement 라는 함수의 파라미터로 container를 전달하면 container에 마운팅된 컴포넌트에 포함된 DOM element를 찾는 쿼리(getByText)들을 반환합니다(source code). getBytext 쿼리는 DOM tree에서 주어진 텍스트와 일치하는 element를 찾고, 존재하면 해당 element를 반환하고 그렇지 않으면 error를 throw 하여 테스트를 실패하게 합니다.

위 테스트 코드가 이전에 작성한 테스트 코드와 어떻게 다른지 비교해보면, DOM element에 접근하는 방식(getByText)이 마치 사용자가 웹 사이트에 접속해서 텍스트를 읽는 행위와 유사합니다. 즉, 애플리케이션이 실제로 사용되는 방식으로 테스트 코드가 작성되었습니다. 나아가 쿼리의 이름만 보고도 무슨 일을 하는지 예상할 수 있고, 테스트 코드의 의도를 명확하게 파악할 수 있게 바뀌었습니다.

Keep DRY principle

그러나 위 테스트 코드에는 여전히 setup/teardown 로직이 남아있습니다. 이 로직이 다른 테스트 코드에서 중복되지 않도록 하기 위해 다음과 같이beforeEach 훅에서 컴포넌트를 마운트할 컨테이너를 생성하는 로직을 render()라는 함수로 추출할 수 있습니다. 또한 afterEach 훅에서 컨테이너에 마운트된 컴포넌트를 언마운트 시키고 컨테이너를 DOM tree에서 제거하는 로직을 cleanup() 함수로 추출할 수 있습니다.

 

이렇게 render(), cleanup() 함수를 만들면 기능을 유지하면서도 다른 테스트 코드에서 해당 함수를 재사용할 수있기 때문에 setup/teardown로직의 중복을 피할 수 있습니다.

Testing React — with @Testing-library/react

위에서 만든 render(), cleanup() 함수와 비슷한 기능을 하는 유틸리티를 @testing-library/react 패키지에서 제공하고 있습니다. 그리하여 위에서 @testing-library/dom으로 작성한 테스트 코드를 @testing-library/react를 활용해서 작성해보면 다음과 같습니다.

 

어떤가요? 테스트 코드가 눈에 띄게 줄어들었습니다. 이는 @testing-library/react에서 제공하는 render 함수가 내부적으로 setup 로직을 수행하고, global context에서 teardown을 처리해주기 때문입니다. 또한 내부적으로 React의 act() 함수를 wrapping 하므로 컴포넌트 렌더링 등의 UI 상호작용 시 명시적으로 act() 함수를 호출할 필요가 없습니다. 그리하여 React 애플리케이션을 테스트할 때 @testing-library/react를 활용하면 불필요한 코드의 중복을 피하고 사용자 입장에서 테스트 코드를 작성할 수 있습니다.

Conclusion

이번 포스팅에서는 React 컴포넌트를 테스트하는 방법에 대해 살펴보았습니다. React에서 제공하는 테스트 유틸리티만 활용한 전통적인 방법부터 @testing-library/dom, @testing-library/react를 활용한 방법까지 알아보았는데요, 이번 포스팅에서 소개한 내용을 정리하면 다음과 같습니다.

  • React 컴포넌트를 테스트할 때 각 테스트가 독립적으로 실행되고 다른 테스트에 영향을 미치지 않도록 하기 위해 setup/teardown하는 로직이 필요합니다.
  • React는 테스트 코드에서 컴포넌트 렌더링, 사용자 이벤트, 데이터 페치 등 UI 상호작용으로 인한 DOM의 변경이 assertion 전에 완료되도록 하는 act() 함수를 제공합니다.
  • @testing-library/dom 패키지를 활용하면 마치 사용자가 애플리케이션을 사용하는 것처럼 테스트 코드를 작성할 수 있고, 그로 인해 작성자의 의도를 명확하게 이해할 수 있으며 궁극적으로 테스트가 PASS 할 때 개발자는 더 많은 신뢰를 얻을 수 있습니다.
  • @testing-library/react 패키지를 활용하면 중복되는 코드(setup/teardown, act, etc.)의 작성을 피할 수 있습니다.