React App Test

kim98111·2022년 6월 21일
0

React

목록 보기
29/29
post-thumbnail

Automated Tests

수동적인 테스팅, 즉 개발자가 코드를 작성해서 특정 속성을 구현하거나 특정 문제를 해결하고 브라우저에서 시험적으로 테스트하는 것을 의미합니다. 이는 개발자로서 항상하는 것입니다. 하지만 이런 수정적인 앱 테스팅은 오류 발생이 쉽습니다. 이는 가능한 모든 조합과 시나리오를 테스트하기 어렵기 때문입니다.
그렇기 때문에 자동화된 테스팅을 사용하는 것입니다. 즉, 기존처럼 수동적인 테스팅을 하면서 추가적으로 자동화된 테스팅을 하여 오류를 더 일찍 잡을 수 있고 더 나은 코드를 작성할 수 있습니다.

자동화된 테스팅은 추가적인 코드를 작성해서 이 코드가 실행되면서 다른 코드를 테스트합니다. 자동화된 테스팅의 장점은 아래와 같습니다.

  1. 전체 애플리케이션을 자동으로 테스트하는 코드를 작성하기 때문에 항상 모든 것을 테스트할 수 있다는 점입니다.

  2. 코드가 변경될 때마다 서로 다른 개별적인 구성요소에 대해 테스트를 진행한 다음 모든 개별 구성요소들을 다같이 테스트합니다.

  3. 모든 것을 상시 테스트 가능하며 수동 테스팅과 함께하면 오류를 더 일찍 잡을 수 있고 더 나은 코드를 작성할 수도 있습니다.

Automated Tests의 종류

자동화 테스트에 여러 다른 종류가 존재합니다. 예를 들어, Unit Test가 존재합니다. 이외에도 Integration Tests, End-to-End(e2e) Tests 등이 존재합니다.

Unit Tests

단위 테스트는 애플리케이션의 가장 작은 단위에 대한 테스트를 작성하는 것입니다. 즉, 애플리케이션에서 사용되는 독립적인 개별 함수(컴포넌트)들을 테스팅하는 것입니다.

따라서 프로젝트에는 일반적으로 많은 단위 테스트가 포함됩니다. 애플리케이션을 구성하는 모든 함수 및 컴포넌트를 테스트하기 때문입니다. 그렇기 때문에 단위 테스트는 가장 일반적이고 중요한 테스트입니다.

아이디어는 모든 개별 단위를 자체적으로 테스트하면 전체 애플리케이션도 동작한다는 것에서 나왔습니다. 하지만 전체 애플리케이션도 실제로 동작하는지 확인하기 위해 이 모든 단위들을 모아서 통함 테스트를 해볼 수 있습니다.

Integration Tests

통합 테스트는 여러 개의 구성요소의 조합을 테스트합니다. 예를 들어, 여러 구성요소가 함께 작동되는지를 테스트합니다.

프로젝트에는 일반적으로 몇 가지 통합 테스트를 포함합니다. 하지만 단위 테스트만큼 많지는 않습니다.

일반적으로 통합 테스트도 매우 중요합니다. 하지만 단위 테스트보단 통합 테스트의 수가 적습니다.

End-to-End Tests

전 구간 테스트는 애플리케이션의 전체 워크플로우, 전체 시나리오를 테스트하는 것입니다. 이는 실제로 사용자가 웹 사이트에서 수행하는 작업을 재현하는 것을 목표로 합니다. 수동 테스트로도 하는 것을 단지 자동화하는 것입니다.

전 구간 테스트는 단위 테스트나 통합 테스트만큼 많지는 않습니다. 단위 및 통합 테스트가 잘 작동된다면 전체적으로 앱이 잘 작동될 것이라고 확신할 수 있기 때문입니다.

Tools & Setup

테스팅 코드를 실행하기 위해 추가적인 도구와 설정이 필요합니다. 구체적으로 말하면 "테스팅 코드를 실행하고 결과를 확인하기 위한 도구"가 필요합니다. 그리고 리액트 앱에서는 "리액트 앱과 컴포넌트들을 렌더링하는 것을 시뮬레이팅"하는 방법도 필요로 합니다.

테스팅 코드를 실행하고 결과를 확인하는 첫 번째 부분에 대해서는 일반적으로 "Jest"를 사용합니다.

리액트 앱에서 컴포넌트를 렌더링하고 시뮬레이팅하는 부분에 대해서는 "React Testing Library"를 주로 사용합니다.

이 두가지 도구(Jest, React Testing Library)들은 react-create-app을 사용하여 프로젝트를 생성했다면 이미 설치와 설정이 되어 있습니다.

위 그림은 react-create-app으로 생성한 프로젝트의 package.json 파일입니다. 이때 dependencies에 testing-library가 존재하는 것을 확인할 수 있습니다. 그리고 testing-library의 하위 패키지로 jest-dom이 존재하는 것도 확인할 수 있습니다.
즉, React Testing Library(RTL)가 Jest를 포함하고 있습니다.

Test

react-create-app으로 프로젝트 생성시 src 폴더 내 App.test.js와 setupTests.js 파일이 존재하는 것을 확인할 수 있습니다.

setupTests.js는 파일 이름이 의미하듯 몇 가지 설정 작업을 합니다. 설정 작업을 제외한 다른 것들은 해당 파일에서 할 필요가 없습니다.

App.test.js 파일에 테스트 코드 일부가 포함되어 있으며, 즉시 사용할 수도 있습니다.

App.test.js 파일은 App 컴포넌트를 테스트하기 위한 파일입니다. 테스팅 파일과 컴포넌트 파일의 이름을 일치시키는 것이 관례이며 파일명 뒤에 "test"만 덧붙여 테스팅 파일명을 작성합니다. 즉, "*.test.js" 확장자를 사용합니다.

App.test.js 파일에서는 test 함수 호출문이 있으며 이는 두 개의 인수를 전달받아 호출되고 있습니다.

첫 번째 인수로는 테스트에 대한 설명이며 이것이 테스팅 출력에서 테스트를 식별하는 것을 도와줍니다.

두 번째 인수로는 함수를 전달합니다. 이는 실제 테스트 코드를 포함하고 있는 함수 참조를 전달해야 합니다. 테스트를 실행하면 실행될 코드입니다.
함수 내부에서는 "@testing-library/react"에서 가져온 render 호출문으로 App 컴포넌트를 렌더링하고, 대소문자 구분하지 않고 "learn react"라는 텍스트를 갖는 엘리먼트가 실제로 문서 내에 존재하는지 확인하는 코드며 존재한다면 테스트가 성공할 것입니다.

테스트를 실행하려면 package.json의 scripts에 작성된 명령어인 "test"를 이용합니다. 즉, 터미널에 "npm test"를 입력하여 테스트를 실행할 수 있습니다.

npm test 명령어를 입력하면 위 그림과 같은 화면이 보이게 됩니다. 실제로 명령어를 입력하는 즉시 테스트를 실행하지는 않습니다.
모든 테스트를 실행하려면 "a"를 입력합니다. "a" 옵션을 선택하면 "*.test.js" 확장자를 가진 모든 파일을 찾고 test 함수를 실행하여 모든 테스트를 실행할 것입니다.

"a"를 입력하면 위 그림처럼 테스트를 실행하고 결과를 보여줍니다. 현재는 1개의 테스트를 실행하였으며 통과했다는 결과를 보여줍니다.

만약 테스트에 실패하게 된다면 아래와 같은 결과가 표시됩니다.

이때 왜 테스트에 실패했는지에 대한 설명도 추가적으로 표시됩니다.

참고로 npm test를 입력하면 watch 모드가 되어 추가적으로 명령어를 실행하지 않아도 파일이 수정되면 자동적으로 인지하여 테스트를 다시 진행하게 됩니다.

Testing

테스트를 하기 위해서는 test 함수를 호출하여 테스트를 진행합니다. 참고로 test 함수는 import하지 않고 전역적으로 사용할 수 있습니다.

test('테스트에 대한 설명', () => { ... 테스트 코드 })

  1. test 함수의 첫 번째 인수로는 앞에서 살펴본것 처럼 테스트에 대한 설명을 작성해줍니다. 일반적으로 테스트가 하는 일을 간단하게 작성합니다.

  2. 두 번째 인수로는 실제 테스트 코드를 포함하는 함수를 전달합니다. 함수 내부에서는 세 가지 과정을 거치게 됩니다.


두 번째 인수로 전달되는 함수 내부에 테스트 코드를 작성할 때는 세 가지 "A" 단계를 거칩니다.

  1. Arange : 첫 번째로 준비가 필요합니다. 예를 들어, 테스트하고자하는 컴포넌트를 렌더링하거나 요구된다면 추가적인 설정도 가능합니다.

  2. Act : 두 번째는 실행입니다. 실제로 테스트하고자하는 것을 작성해줍니다. 예를 들면, 버튼 클릭을 시뮬레이션 해보고 싶다면 두 번째 단계에서 실행하는 것입니다.

  3. Assert : 마지막으로는 결과를 단언하는 것입니다. 예를 들면, 브라우저상에서 보이는 결과를 검토하는 것입니다. 즉, 실행 결과와 우리의 예상과 일치하는지 비교해 보는 것입니다.

즉, "컴포넌트 렌더링 -> 특정 액션 발생 -> 결과 확인" 의 순서로 테스트 과정을 거칩니다.


위 테스트 코드는 Arange 단계에서 Greeting 컴포넌트를 렌더링하고, Assert 단계에서 Hello World라는 텍스트를 갖는 엘리먼트가 존재의 유무를 테스트 결과로 사용하고 있습니다.

  1. Arange : 테스트를 위해 render 함수의 인수로 컴포넌트를 전달하여 컴포넌트를 jsdom에 렌더링

  2. Act : 특정 액션 발생 X

  3. Assert : screen.getByText 쿼리 메서드로 현재 렌더링된 엘리먼트 중 "Hello World" 문자열을 갖는 엘리먼트를 찾아 helloWorldElement 변수에 할당하고, 결과가 예상대로 나왔는지 확인하기 위해 expect 함수의 인수로 엘리먼트를 전달하고 macher 메서드인 toBeInTheDocument 함수를 호출하여 돔에 존재하는지 유무를 통해 테스트 결과를 결정합니다.

Query Method

돔에 렌더링된 엘리먼트를 찾는 메서드를 Query 메서드라고 하며, getByText와 같은 메서드를 의미합니다. 쿼리 메서드는 3가지 섹션으로 나누어져 있습니다.

  • 쿼리타입/타겟 개수/타겟 유형

    1. 쿼리 타입

      • get: 동기적으로 돔 엘리먼트 검색, 타겟 못찾을시 에러

      • find: 비동기적으로 돔 엘리먼트 검색, 기본값 4500ms 이후에도 타겟 못찾을시 에러, 결과를 resolve한 프로미스 객체를 반환

      • query: 동기적으로 돔 엘리먼트 검색, 타겟 못찾을시 null 반환

    2. 타겟 개수

      • 다수의 엘리먼트라면 쿼리 타입 다음에 "All"을 작성, 다수의 엘리먼트를 요소로 갖는 배열을 반환. findAll* 사용시 배열을 resolve한 프로미스 객체를 반환
    3. 타겟 유형(사용 우선순위)

      • ByLabelText
        : label이 있는 input 의 label 값으로 input 엘리먼트를 선택

      • ByPlaceholderText
        : placeholder 값으로 input 및 textarea 엘리먼트를 선택

      • ByText
        : 엘리먼트가 가지고 있는 텍스트 값으로 DOM을 선택합니다.

      • ByDisplayValue
        : input, textarea, select가 지니고 있는 현재 값을 가지고 엘리먼트를 선택합니다.

      • ByAltText
        : alt 속성을 가지고 있는 엘리먼트(주로 img) 를 선택합니다.

      • ByTitle
        : title 속성을 가지고 있는 DOM 혹은 title 엘리먼트를 지니고있는 SVG 를 선택 할 때 사용합니다.

      • ByRole
        : 특정 role 값을 지니고 있는 엘리먼트를 선택합니다.

      • ByTestId
        : 다른 방법으로 못 선택할때 사용하는 방법입니다. 특정 DOM 에 직접 test 할 때 사용할 id 를 달아서 선택하는 것을 의미합니다. 즉, data-testid 어트리뷰트 값을 통해 엘리먼트를 선택합니다.

쿼리 메서드를 사용할 때는 위 그림처럼 위에서 아래 순서로 결정하여 사용합니다.

Test matcher

  1. .toBe(원시값) : 인수로 전달한 원시값과 일치시 테스트 성공

  2. .toEqual(객체) : 인수로 전달한 객체의 구조와 일치한 경우 테스트 성공

  3. .toBeTruthy() : truthy 값인 경우 테스트 성공

  4. .toBeFalsy() : falsy 값인 경우 테스트 성공

  5. .toBeInTheDocument() : 돔에 엘리먼트 존재시 테스트 성공

  6. .toContain(값) : 인수로 전달된 값이 배열에 존재하는 경우 테스트 성공

  7. .toMatch(정규표현식) : 인수로 전달된 정규표현식의 패턴과 매치하는 경우 테스트 성

  8. .not : Matcher 함수 불만족시 테스트 성공

  9. .toHaveLength(정수) : 배열의 length 프로퍼티값과 인수로 전달된 값 일치시 테스트 성공

User Actions

위 테스트 코드에서는 click 이벤트를 발생시키며 테스트를 진행하는 코드입니다.

  1. Arange : render 함수를 통해 Greeting 컴포넌트를 렌더링

  2. Act : "@testing-library/user-event"에서 userEvent를 import하고, userEvent로 click 메서드를 호출하여 이벤트를 발생

  3. Assert : screen.getByText로 엘리먼트를 가져온 뒤 해당 엘리먼트가 존재하는지를 테스트 결과로서 사용합니다.


테스트 코드를 포함하는 함수는 async 함수로 정의하는 것이 가능하며 비동기 로직도 테스트하는 것이 가능합니다.

  1. Arange : Async 컴포넌트를 render 함수로 렌더링

  2. Act : X

  3. Assert : findAllByRole 쿼리 메서드는 비동기로서 동작하며 찾은 엘리먼트들을 요소로 갖는 배열을 resolve한 프로미스 객체를 반환합니다. 해당 배열의 길이가 0이 아니라면 테스트를 통과하도록 테스트 결과를 사용합니다.

Test Suites & Tests

애플리케이션의 규모가 커질수록 수십 개 혹은 수천 개의 테스트 파일을 갖게 될텐데 이러한 다수의 테스트들을 서로 다른 Test Suite에 넣어서 그룹화하여 정리할 수 있습니다.

test suite는 describe 함수로 생성할 수 있으며 전역적으로 사용되기 때문에 import하지 않고 사용할 수 있습니다.

describe 함수는 두 개의 인수를 전달받습니다. 첫 번째 인수로는 설명을 전달합니다. 서로 다른 테스트들이 어디에 속할 것인지에 관한 카테고리 설명에 해당합니다.

두 번째 인수로는 함수를 전달하는데 이 함수에는 테스팅 코드를 작성하지 않고 다른 테스트들을 작성해줍니다. 즉, 함수 내부에 test 호출문을 작성해줍니다.

Greeting.test.js에서 describe 함수로 Greeting Component Test Suite를 생성하였습니다. 그리고 테스팅을 시작하면 아래와 같은 테스트 결과가 표시됩니다.

describe 함수의 첫 번째 인수로 전달한 문자열 Greeting Component 하위에 renders Hello world as a text 테스트가 표시되어 있습니다.

즉, describe 함수의 첫 번째 인수로 전달한 문자열이 suite를 구별하는 식별자처럼 사용됩니다.

Async Test(비동기 테스트)

HTTP 요청을 보내는 코드를 테스트할 때 일반적으로 서버에 HTTP 요청을 보내지 않습니다. 이는 아래와 같은 이유로 요청을 보내지 않습니다.

  1. 많은 네트워크 트래픽을 일으키기 때문에 서버가 요청들에 의해 과부화될 것입니다. 특히 많은 테스트에서 요청을 보내는 경우가 있습니다.

  2. 데이터를 가져오지는 않지만 일부 컴포넌트가 서버로 POST 요청을 전송한다면 테스트로 인해 데이터베이스에 데이터가 추가될 것입니다. 혹은 서버의 내용이 변경될 수도 있습니다.

이러한 이유로 인해 테스트 코드를 작성할 때는 실제로 HTTP 요청을 전송하지 않거나 혹은 일종의 테스팅 서버로 요청을 전송하는 것입니다.


두 가지 방법 모두 사용가능하며 이번에는 HTTP 요청을 전송하지 않는 방법에 대해서 알아보겠습니다.

Async 컴포넌트는 useEffect 훅을 이용하여 API에 HTTP 요청을 전송하고 응답으로 데이터를 받은 뒤 상태 변경 함수를 호출하고 있습니다. 이때 요청을 보낼 때 Fetch API를 사용하여 HTTP 요청을 전송하고 있습니다.

Async.test.js 테스트 파일의 테스트 코드 내부 Arrange 단계에서 fetch 함수를 재정의하는 것입니다. 이때 window.fetch에 jest.fn()의 반환값을 할당합니다. jest.fn()은 더미 함수(가짜 함수)를 생성하여 반환합니다.

그리고 jest.fn()의 반환값인 더미 함수를 할당받은 window.fetch로 mockResolvedValueOnce 메서드를 호출하는데 이는 resolve될 값을 인수로 전달합니다. 여기서는 응답 객체(response)의 역할을 하는 객체를 mockResolvedValueOnce 메서드의 인수로 전달합니다. 이때 전달되는 객체 내부에서는 json이라는 이름의 메서드도 정의해주어야 합니다. 이는 Async 컴포넌트에서 response.json()을 호출하기 때문입니다.

이렇게 우리는 테스트를 할 때 실제로 HTTP 요청을 보내지 않고 HTTP 요청을 보내는 fetch 함수를 HTTP 요청을 보내지 않는 함수로 재정의하였습니다.


.mockResolvedValueOnce 이외에도 다양한 메서드들이 존재합니다.

  • .mockReturnValue(value) : 함수가 호출될 때마다 반환될 값을 지정

  • .mockReturnValueOnce(value) : 함수가 한 번 호출될 때 반환될 값 지정

  • .mockImplemetation(() => { ... }) : 함수를 즉석으로 구현할 수 있다. 인수로 구현될 함수를 전달한다.

  • .mockImplementationOnce(() => { ... }) : 함수가 한 번 호출될 때 실행될 함수의 구현 전달

  • .mockResolvedValue(value) / .mockRejectedValue(value)
    : 비동기 함수에서 resolve 값/reject 값을 인수로 전달한다.

  • .mockResolvedValueOnce(value) / .mockRejectedValueOnce(value) : 비동기 함수에서 호출시 한 번 resolve 값/reject 값을 인수로 전달한다.

profile
Frontend Dev

0개의 댓글