React.js Testing

강정우·2023년 2월 4일
0

react.js

목록 보기
41/45
post-thumbnail

1. 테스팅에 대하여

Manual 테스팅의 문제점

  • 우리는 살면서 테스트 코드가 매우 종요하다고 귀에 못이 박히도록 들었다.
    그럼 testing을 정확히 무엇일까?

  • 이미 우리 과정에서 많은 테스트들을 해봤다. 다만 이것은 Manual한 테스팅이었다.
    다만 이 방법은 오류 발생이 쉽다 Manual한 방법으로는 가능한 모든 조합과 시나리오를 테스트하기 어렵기 때문이다.

  • 우리가 어떤 것을 변경한 것이 앱의 다른 속성을 손상시킬 수 있다
    그리고 우리가 모든 것을 항상 테스팅 하는 것이 아니기 때문에 이 버그를 그냥 지나칠 수 있다
    이는 추가적인 소요가 많이 들어갈 수 있다. 그렇기 때문에 자동화된 테스팅을 하는 것이다.

Automated 테스팅의 장점

  • 전제 애플리케이션을 자동으로 테스트하는 코드를 작성하기 때문에 짧은 시간안에 모든 것을 테스트할 수 있다.

  • 또한 앱의 서로 다른 개별 구성요소에 대한 테스트를 한다.
    그 다음 코드를 변경할 때마다 이 모든 개별 구성요소들을 다같이 테스트한다.

  • 하지만 Manual 테스팅을 사용하면 안된다는 것을 절대 아니다.
    항상 수동 테스팅과 함께하여 오류들을 훨씬 더 일찍 잡을 수 있고 더 나은 코드를 작성할 수 있다.

2. 테스팅의 종류

Unit Tests

  • 단위 테스트는 애플리케이션의 가장 작은 단위에 대한 테스트를 작성하는 것이다.

  • 여기서 가장 작은 단위는 개별 함수들이 될 수 있고 리액트의 경우엔 일부 컴포넌트를 뜻할수 있다.

  • 즉, 프로젝트에는 일반적으로 많은 단위 테스트가 포함된다.
    애플리케이션을 구성하는 모든 단위, 모든 함수 및 컴포넌트를 테스트 하기 때문이다.

  • 그렇기 때문에 단위 테스트는 가장 일반적이고 중요한 종류의 테스트이다.

Integration Tests

  • 통합테스트는 여러개의 구성 요소의 조합을 테스트하는 것이다.

  • 예를 들어 여러 구성요소가 함께 작동 되는지를 확인한다.

  • 프로젝트에는 일반적으로 몇가지 통합테스트가 포함된다.
    하지만 당연히 단위테스트 만큼 많지는 않다

  • 리액트 앱은 컴포넌트 간의 결합이 긴밀하기 때문에 태스팅 할 때 단위테스트와 통합테스트를 구별하는 것이 어렵다.

End-to-End (e2e) Tests

  • 전 구간 테스트, 애플리케이션의 전체 워크플로우를 테스트하는 것이라 할 수 있다.

  • 예를들어 사용자가 로그인하고 특정 페이지로 이동하는 것과 같이 말이다.
    실제로 사용자가 우리의 웹사이트에서 수행하는 작업을 재현하는 것을 목표로 한다.

  • 수동 테스트로도 하는 것을 단지 자동화하는 것이다. 하지만 이때 특정 시나리오가 아닌 모든 시나리오에 대하여 자동으로 테스트 해준다.

  • 당연하게도 단위테스트와 통합테스트 만큼 많지는 않다 왜냐하면 단위 및 통합 테스트가 잘 작동한다면 전제적으로 앱이 잘 작동한다고 꽤 확신할 수 있기 때문이다.

  • 단위 및 통합 테스트가 전구간 테스트보다 보다 더 쉽다.
    하지만 보통 더 빠르고 집중적이며, 그리고 가능한 모든 시나리오를 테스트하는 것이 훨씬 쉽다.

3. 무엇을 테스팅 할까?

  • 테스팅할 때 항상 생각해야할 것이 바로 What과 How이다.

무엇을?

  • 우리는 테스팅 코드에 어떤 종류의 코드를 넣어야 할지 고민해야한다.

  • 서로 다른 기본 구성요소를 테스트 해보아야 하며 정말 작은 구성요소들도 테스트해야 한다.

  • 작고, 집중된 테스트로 각각 하나의 주요 사항부터 테스트하는 것이다.
    여러개의 집중된 테스트를 가짐으로써 실패한 구체적인 이유를 알 수 있다.

어떻게?

  • 이제 어떻게 테스트 할지에 관해서는 사용자가 우리의 앱과 상호작용 했을 때 발생할 수 있는 성공 및 오류 사례를 테스트한다.

  • 그리고 물론 드물지만 가능한 시나리오와 결과도 테스트 해야 한다.

Tools & Setup

  • 테스팅은 그냥하는 것이 아닌 몇가지 결과를 성공으로 볼 수 있는지,
    또는 결과로 미루어 봤을때 테스트가 실패했는지 여부를 판단하기 위한 도구가 필요하다.
    그리고 리액트 앱에서는 리액트 앱과 컴포넌트들을 렌더링하는 것을 시뮬레이팅 하는 방법이 필요하다. 이는 브라우저를 시뮬레이팅 하는 것이라고 할 수 있다

  • 테스팅 코드를 실행하고 결과를 확인하는 첫번째 부분에 대해서는 보통 jest를 사용한다.
    이 작업을 위한 유일한 도구는 아니지만 매우 인기 있고 사용하기도 쉽다.

  • 리액트 앱에서 컴포넌트를 렌더링하고 시뮬레이팅 하는 부분에 대해서는 요즘에는 리액트 테스팅 라이브러리를 주로 사용한다 .

  • 이 두가지 도구는 create-react-app으로 프로젝트를 시작했다면 이미 설치 및 설정이 되어 있을 것이다

  • 이제 우리는 단 한번도 보지 않았던 2개의 Test 파일로 시작을 해볼 것이다.

  • setupTests.js 파일은, 이름에서 알 수 있듯이 단지 설정하는 작업을 한다.
    그렇기 때문에 이 파일 안에는 아무것도 하지 않아도 된다.

  • 그렇지만 App.test.js 파일은 자세히 들여다 봐야한다.
    이 파일이 테스팅 코드를 포함하는 파일이다.

  • App.test.js 파일이 이 App.js 컴포넌트를 테스트하기 위해 있는 파일이다.
    테스팅 파일의 이름은 컴포넌트 파일과 같이 짓는 것이 관례이다.

  • 그 파일명에 test 만 붙이면 된다.
    정확히 말하면 .test.js를 확장자로 붙이는 것이다.

  • 그리고 이 App.test.js 파일에는 test 함수가 있는데 두개의 인자를 갖는다.
    • 첫번째 인자는 테스트에 대한 설명이다. 이부분은 우리 마음대로 하면 된다.
      테스트 출력에서 이 테스트를 식별하는데 도움이 된다.
    • 두번째 인자는 함수이고 실제 테스트와 코드를 포함하고 있는 부분이다.
      따라서 이 코드가 우리가 테스트를 실행할 때 실행될 코드이다.
    • 이때 함수중에 마지막에는 이 App 컴포넌트를 렌더링한다.
      그런 일부 요소를 가상의 화면(시뮬된 브라우저)이라 할 수 있는 것에 뿌려지도록 한다.
      거기에 이 App이 렌더링 되고 추가로 몇가지 요소를 뿌려준다.
    • 그리고 요소를 식별할 때 그 안에서 렌더링되는 텍스트로 식별한다.
      이때 대소문자 구분은 없다.
      /learn react/ 이건 정규 표현이고
    • toBeInTheDocument 는 이 요소가 실제로 문서에 있는지를 확인한다.
      그래서 이 테스트는 해당 요소를 찾지 못하는 경우 실패이고, 찾을 경우 성공이게 되는 것이다.
  • 그렇다면 이 테스트는 어떻게 실행할까
    바로 터미널에서 npm test 를 쳐주기만 하면 된다.

  • 바로 결과값을 확인할 수 있다.
  • V 옆엔 우리가 써놓았던 test설명을 확인할 수 있다.
  • 또한 이땐 서버가 실행중일 때 저장하면 바로 반영되는 것 처럼 test도 저장하면 바로 테스트를 한다.
  • 그리고 만약 테스트가 실패라면 왜 실패인지도 설명으로 알려준다.
  • 그리고 테스팅 코드를 볼 수 있는데 실패한 줄을 볼 수 있습니다, 이 경우엔 .getByText()부분인데 이 부분이 실패한것이다. 그럼 이제 개발자로서 우리는 테스트를 다시 조정할 수도 있고 본문 코드를 조정할 수도 있다.

  • 그리고 당연하게 나갈땐 Ctrl+C로 나갈 수 있다.

작성법

  • 통상 테스트 대상 파일과 테스트 파일은 바로 옆에다가 만든다.
test()
  • 여기서의 test함수는 그냥 js함수이다. 그리고 2개의 매개변수를 넣는데
    첫번째는 앞서 설명했듯 그냥 해당 test에 대한 설명이고
    두번째는 실제 테스트 함수를 작성해주면되는데 이때 지겨야할 "3A"가 있다.
  1. Arrange (준비)
  • 우리는 테스트를 설정하고자 하는 건데 예를 들면, 테스트하고자 하는 컴포넌트를 렌더링하길 원하는 것이다.
    필요하다면 추가 설정도 할 수 있다.
  • 예를 들어 <컴포넌트> 를 렌더함수에 집어넣어 테스트를 위한 렌더링할 때 하위에 있는 모든 컴포넌트 까지 모두 불러온다는 것이다. 물론 이 경우엔 unit이 아니라 Integration Test가 되겠지만 말이다.
  1. Act (실행)
  • 실제로 테스트하고자 하는 걸 하는 것이다.

  • 예를 들면, 버튼 클릭을 시뮬레이션 해보고 싶다면 실행하는 것 처럼

  1. Assert (단언)
  • 예를 들면, 브라우저상에서 보이는 아웃풋을 검토하는 과정이다.
    그런 다음에 우리의 예상과 같은지 보는 것이다.
import Greeting from "./Greeting";
test("renders Hello World as a text", () => {
    //준비
    render(<Greeting/>)
    // 실행

    // 확인
    const 변수 = screen.getByText("Hello World", {exact:true});
  	expect(변수).Matcher함수();
});
  • 렌더링 된 가상 DOM 또는 가상 화면에 액세스할 수 있게 해주는 screen을 불러올 수 있다.
    그런 다음 이 screen을 사용해서 화면에서 엘리먼트들을 찾을 수 있다.

  • 또한 screen 객체가 사용할 수 있는 여러 가지 함수가 있는데
    get 함수, find 함수, 그리고 query 함수가 있다.

  • 이 함수들의 주요 차이점은 이 함수들이 에러를 냈을 때 promise를 반환하느냐 아니냐의 차이가 있다.

    • get 함수가 에러를 발생시켜서 엘리먼트를 찾을 수 없다면
    • query 함수는 에러를 방생시키진 않는다.
    • find 함수는 promise를 반환한다.
  • const 변수 에 담은 이유는 getByText함수가 실패한다면 get함수의 특징상 에러가 날테니 일부러 변수에 담았다.

  • 담은 변수를 expect 함수로 가져온다. 이때 변수로 숫자, 문자열 등 아무거나 다 된다.

  • 그리고 expect 함수의 결과에 여러 matcher함수로 테스팅을 확인할 수 있다.

  • 예를 들어 toBeInTheDocument 함수는 우리가 여기에 있을 것으로 예상하는 HTML 엘리먼트가 문서 안에 있는지 확인하는 함수이다.

Test Suites

  • 우리의 애플리케이션 규모가 커질수록 아마 수십 개 또는 수백, 수천 개의 테스트를 갖게 될 텐데 이러한 다수의 다른 테스트를 서로 다른 테스트 suite에 넣어서 그룹화하고 정리한다.
  • 예를 들어, 애플리케이션 내의 하나의 특징 또는 하나의 컴포넌트에 속하는 모든 테스트는 한 테스트 suite 그룹에 들어간다는 것이다.
describe("Greeting component", ()=>{
    test(),
    test(),
    test()...
})
  • 또한 describe함수를 통해 test suite를 생성한다.
    여기에 오는 두 매개변수 중 첫 번째 매개변수에는 설명이 온다. 서로 다른 테스트들이 어디에 속할지에 관한 카테고리 설명에 해당한다.
    그런 다음 두 번째 매개변수에도 익명의 함수가 오는데 이 함수에는 자체 테스트 코드를 쓰지 않고 다른 테스트들을 넣는다.

  • suite를 여러 개 가질 수도 있으며 suite마다 테스트도 여러 개 가질 수 있다.

useEffect, sideEffects(http request...) 처리

describe("Async component", ()=>{
    test("renders posts if request succeeds",async ()=>{
        render(<Async/>)
        const listItemElement = await screen.findAllByRole("listitem");
        expect(listItemElement).not.toHaveLength(0);
    });
});
  • 이때 getByRole의 자세한 HTML이 맡을 수 있는 역할들을 알고싶다면 다음 홈페이지로 가면 된다.

  • 여기서 우리 코드중에 HTTP 요청이 있다고 가정하자. 이는 비동기 동작이다.
    따라서 포스트를 즉각 가져오지는 않고 대신 이 컴포넌트가 바로 렌더링 된다.
    즉 처음에는 포스트가 존재하지 않는 빈 배열로 렌더링 되는데 여기서 오류를 야기한다.

  • 첫 렌더 사이클이 지나간 후에 이 이펙트가 즉시 실행되고 요청이 전송되어 응답이 돌아오고 상태가 업데이트되면 이 컴포넌트가 리렌더링 된다. 이때야 비로소 리스트 아이템이 존재한다.

  • 즉, 포스트 데이터를 가져오는 데에 몇 밀리초 혹은 몇 초의 시간이 걸리기 때문인데 그게 문제이다.
    getAllByRole을 쓰면 screen의 아이템들을 즉시 가져오게 되는데 초기에는 아무것도 없기 때문이다.

  • 해결법은 앞서 find는 promise를 반환한다고 하였기 때문에 find함수로 findByRole를 사용하고 promise 반환을 보장하려면 async와 await를 붙여주면 된다.

문제점

  • 하지만 여기서 우리는 또한가지 사실을 알고있어야 한다.
    통상적으로 test할 때는 서버에 http request를 하지 않는다.
  1. 많은 네트워크 트래픽을 일으켜서 서버가 요청들로 인해 과부하될 것이기 때문이다.

    • 예를들어 많은 request에 대한 많은 테스트가 존재
  2. 데이터를 가져오지는 않지만, 일부 컴포넌트가 서버로 포스트 요청을 전송한다면 테스트로 인해 데이터베이스에 데이터가 실제로 삽입되거나 혹은 서버의 내용이 변경될 수도 있기 때문

    • 왜냐하면 그러한 종류의 요청이 전송되는 컴포넌트와 시나리오도 테스트해야 할 테니까

그렇다면 우리는 진짜 요청을 전송하지 않거나 일종의 테스팅 서버로 요청을 전송하는 2가지 스탠스를 취할 수 있다.

그러다면 만약 전송을 하지 않는다면 무책임하다고 생각할 수 있는데 사실 테스트를 작성할 때는 내가 작성하지 않은 코드를 테스트해서는 안 된다. 그래서 전송을 하지 않는 것도 하나의 방법이라고 하는 것이다.

그럼 테스팅할 때 fetch함수들을 어떻게 해야할까? => 소위 mock 함수라고 불리는 함수로 대체해야한다.

describe("Async component", ()=>{
    test("renders posts if request succeeds",async ()=>{
        window.fetch = jest.fn();				// 1.
        window.fetch.mockResolvedValueOnce({
            json: async () => [{id:"p1",title:"First post"}]				// 2.
        });
        render(<Async/>);
        const listItemElement = await screen.findAllByRole("listitem");
        expect(listItemElement).not.toHaveLength(0);
    })
})
  1. 테스트를 준비할 때 여기에서 내장 fetch 함수를 우리만의 다른 함수로 덮어써야 한다.
    이를 위해서 window 객체에서 fetch 메소드를 사용하여 jest객체를 불러와서 몇가시 유틸리티 메서드를 갖는데 여기서는 fn 메서드를 사용할 수 있다.

  2. 이제 이 목업함수로 특수 메서드를 호출하여 작업할 수 있다.
    fetch 함수가 호출되었을 때 결정되어야 하는 값을 설정할 수 있게 해준다.
    이때 production 코드에서 fetch의 결과값을 json을 받아왔다면 여기 test 에서도 json을 받았다~ 라고 코드를 짜워야한다.

장점

  • API에 요청을 전송하고 있지 않아서 해당 API에 불필요한 요청을 보내 과부하가 걸리는 일도 생기지 않게 되었고 네트워크 트래픽 양도 감소하게 됐으며 서버가 다운됐을 때 발생 가능한 잠재적 문제도 피할 수 있다.

  • 또한 fetch 함수의 다양한 결과를 제어해서 테스트의 다양한 시나리오들을 시험해볼 수 있다.

참고


에러

1. render함수 인식 오류

  • react 18버전부터는 render가 지원하지 않는다고 적혀있다. 이때 해결방법 2가지 를 알려주겠다.

1) testing library 버전 업데이트

패키지 버전 13.0.0은 새 createRootAPI에 대한 지원을 추가함.

프로젝트의 루트 디렉터리에서 터미널을 열고 다음 명령을 실행

npm install @testing-library/react@latest
npm install @testing-library/jest-dom@latest
npm install @testing-library/user-event@latest

2) index.js 에 새 createRootAPI 적용

index.js 파일은 새 createRootAPI를 사용하여 애플리케이션을 렌더링해야 함.

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<App />);

2. userEvent 인식 오류

  • 이 또한 jest등 테스트 관련 라이브러리들이 최신 버전으로 올라오면서 생긴 오류들이다.

  • 이러한 오류가 떴다. 참고로 npm run하여 Manual하게 테스트 해봤을 땐 정상적으로 clicked라고 바뀌는데 이 코드에서는 그대로 IT's good to see you!라고 안 바뀐 상태로 있었다. 그렇다면 바로 clicked가 문제였던것

  • 바로 userEvent는 비동기로 처리해주어야한다는 것이다.

test('renders "Changed!" if the button was clicked',async ()=>{
  // Arange
  render(<Greeting/>)
  // Act
  const buttonEle = screen.getByRole('button');
  await userEvent.click(buttonEle);
  // Assert
  const outputElement = screen.getByText("Changed!");
  expect(outputElement).toBeInTheDocument();
});
profile
智(지)! 德(덕)! 體(체)!

0개의 댓글