우리는 살면서 테스트 코드가 매우 종요하다고 귀에 못이 박히도록 들었다.
그럼 testing을 정확히 무엇일까?
이미 우리 과정에서 많은 테스트들을 해봤다. 다만 이것은 Manual한 테스팅이었다.
다만 이 방법은 오류 발생이 쉽다 Manual한 방법으로는 가능한 모든 조합과 시나리오를 테스트하기 어렵기 때문이다.
우리가 어떤 것을 변경한 것이 앱의 다른 속성을 손상시킬 수 있다
그리고 우리가 모든 것을 항상 테스팅 하는 것이 아니기 때문에 이 버그를 그냥 지나칠 수 있다
이는 추가적인 소요가 많이 들어갈 수 있다. 그렇기 때문에 자동화된 테스팅을 하는 것이다.
전제 애플리케이션을 자동으로 테스트하는 코드를 작성하기 때문에 짧은 시간안에 모든 것을 테스트할 수 있다.
또한 앱의 서로 다른 개별 구성요소에 대한 테스트를 한다.
그 다음 코드를 변경할 때마다 이 모든 개별 구성요소들을 다같이 테스트한다.
하지만 Manual 테스팅을 사용하면 안된다는 것을 절대 아니다.
항상 수동 테스팅과 함께하여 오류들을 훨씬 더 일찍 잡을 수 있고 더 나은 코드를 작성할 수 있다.
단위 테스트는 애플리케이션의 가장 작은 단위에 대한 테스트를 작성하는 것이다.
여기서 가장 작은 단위는 개별 함수들이 될 수 있고 리액트의 경우엔 일부 컴포넌트를 뜻할수 있다.
즉, 프로젝트에는 일반적으로 많은 단위 테스트가 포함된다.
애플리케이션을 구성하는 모든 단위, 모든 함수 및 컴포넌트를 테스트 하기 때문이다.
그렇기 때문에 단위 테스트는 가장 일반적이고 중요한 종류의 테스트이다.
통합테스트는 여러개의 구성 요소의 조합을 테스트하는 것이다.
예를 들어 여러 구성요소가 함께 작동 되는지를 확인한다.
프로젝트에는 일반적으로 몇가지 통합테스트가 포함된다.
하지만 당연히 단위테스트 만큼 많지는 않다
리액트 앱은 컴포넌트 간의 결합이 긴밀하기 때문에 태스팅 할 때 단위테스트와 통합테스트를 구별하는 것이 어렵다.
전 구간 테스트, 애플리케이션의 전체 워크플로우를 테스트하는 것이라 할 수 있다.
예를들어 사용자가 로그인하고 특정 페이지로 이동하는 것과 같이 말이다.
실제로 사용자가 우리의 웹사이트에서 수행하는 작업을 재현하는 것을 목표로 한다.
수동 테스트로도 하는 것을 단지 자동화하는 것이다. 하지만 이때 특정 시나리오가 아닌 모든 시나리오에 대하여 자동으로 테스트 해준다.
당연하게도 단위테스트와 통합테스트 만큼 많지는 않다 왜냐하면 단위 및 통합 테스트가 잘 작동한다면 전제적으로 앱이 잘 작동한다고 꽤 확신할 수 있기 때문이다.
단위 및 통합 테스트가 전구간 테스트보다 보다 더 쉽다.
하지만 보통 더 빠르고 집중적이며, 그리고 가능한 모든 시나리오를 테스트하는 것이 훨씬 쉽다.
우리는 테스팅 코드에 어떤 종류의 코드를 넣어야 할지 고민해야한다.
서로 다른 기본 구성요소를 테스트 해보아야 하며 정말 작은 구성요소들도 테스트해야 한다.
작고, 집중된 테스트로 각각 하나의 주요 사항부터 테스트하는 것이다.
여러개의 집중된 테스트를 가짐으로써 실패한 구체적인 이유를 알 수 있다.
이제 어떻게 테스트 할지에 관해서는 사용자가 우리의 앱과 상호작용 했을 때 발생할 수 있는 성공 및 오류 사례를 테스트한다.
그리고 물론 드물지만 가능한 시나리오와 결과도 테스트 해야 한다.
테스팅은 그냥하는 것이 아닌 몇가지 결과를 성공으로 볼 수 있는지,
또는 결과로 미루어 봤을때 테스트가 실패했는지 여부를 판단하기 위한 도구가 필요하다.
그리고 리액트 앱에서는 리액트 앱과 컴포넌트들을 렌더링하는 것을 시뮬레이팅 하는 방법이 필요하다. 이는 브라우저를 시뮬레이팅 하는 것이라고 할 수 있다
테스팅 코드를 실행하고 결과를 확인하는 첫번째 부분에 대해서는 보통 jest
를 사용한다.
이 작업을 위한 유일한 도구는 아니지만 매우 인기 있고 사용하기도 쉽다.
리액트 앱에서 컴포넌트를 렌더링하고 시뮬레이팅 하는 부분에 대해서는 요즘에는 리액트 테스팅 라이브러리를 주로 사용한다 .
이 두가지 도구는 create-react-app으로 프로젝트를 시작했다면 이미 설치 및 설정이 되어 있을 것이다
setupTests.js 파일은, 이름에서 알 수 있듯이 단지 설정하는 작업을 한다.
그렇기 때문에 이 파일 안에는 아무것도 하지 않아도 된다.
그렇지만 App.test.js 파일은 자세히 들여다 봐야한다.
이 파일이 테스팅 코드를 포함하는 파일이다.
App.test.js 파일이 이 App.js 컴포넌트를 테스트하기 위해 있는 파일이다.
테스팅 파일의 이름은 컴포넌트 파일과 같이 짓는 것이 관례이다.
그 파일명에 test 만 붙이면 된다.
정확히 말하면 .test.js
를 확장자로 붙이는 것이다.
/learn react/
이건 정규 표현이고toBeInTheDocument
는 이 요소가 실제로 문서에 있는지를 확인한다.npm test
를 쳐주기만 하면 된다.V
옆엔 우리가 써놓았던 test설명을 확인할 수 있다.test()
<컴포넌트>
를 렌더함수에 집어넣어 테스트를 위한 렌더링할 때 하위에 있는 모든 컴포넌트 까지 모두 불러온다는 것이다. 물론 이 경우엔 unit이 아니라 Integration Test가 되겠지만 말이다.실제로 테스트하고자 하는 걸 하는 것이다.
예를 들면, 버튼 클릭을 시뮬레이션 해보고 싶다면 실행하는 것 처럼
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를 반환하느냐 아니냐의 차이가 있다.
const 변수
에 담은 이유는 getByText함수가 실패한다면 get함수의 특징상 에러가 날테니 일부러 변수에 담았다.
담은 변수를 expect
함수로 가져온다. 이때 변수로 숫자, 문자열 등 아무거나 다 된다.
그리고 expect 함수의 결과에 여러 matcher함수로 테스팅을 확인할 수 있다.
예를 들어 toBeInTheDocument
함수는 우리가 여기에 있을 것으로 예상하는 HTML 엘리먼트가 문서 안에 있는지 확인하는 함수이다.
describe("Greeting component", ()=>{
test(),
test(),
test()...
})
또한 describe함수를 통해 test suite를 생성한다.
여기에 오는 두 매개변수 중 첫 번째 매개변수에는 설명이 온다. 서로 다른 테스트들이 어디에 속할지에 관한 카테고리 설명에 해당한다.
그런 다음 두 번째 매개변수에도 익명의 함수가 오는데 이 함수에는 자체 테스트 코드를 쓰지 않고 다른 테스트들을 넣는다.
suite를 여러 개 가질 수도 있으며 suite마다 테스트도 여러 개 가질 수 있다.
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를 붙여주면 된다.
많은 네트워크 트래픽을 일으켜서 서버가 요청들로 인해 과부하될 것이기 때문이다.
데이터를 가져오지는 않지만, 일부 컴포넌트가 서버로 포스트 요청을 전송한다면 테스트로 인해 데이터베이스에 데이터가 실제로 삽입되거나 혹은 서버의 내용이 변경될 수도 있기 때문
그렇다면 우리는 진짜 요청을 전송하지 않거나 일종의 테스팅 서버로 요청을 전송하는 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);
})
})
테스트를 준비할 때 여기에서 내장 fetch 함수를 우리만의 다른 함수로 덮어써야 한다.
이를 위해서 window 객체에서 fetch 메소드를 사용하여 jest객체를 불러와서 몇가시 유틸리티 메서드를 갖는데 여기서는 fn 메서드를 사용할 수 있다.
이제 이 목업함수로 특수 메서드를 호출하여 작업할 수 있다.
fetch 함수가 호출되었을 때 결정되어야 하는 값을 설정할 수 있게 해준다.
이때 production 코드에서 fetch의 결과값을 json을 받아왔다면 여기 test 에서도 json을 받았다~ 라고 코드를 짜워야한다.
API에 요청을 전송하고 있지 않아서 해당 API에 불필요한 요청을 보내 과부하가 걸리는 일도 생기지 않게 되었고 네트워크 트래픽 양도 감소하게 됐으며 서버가 다운됐을 때 발생 가능한 잠재적 문제도 피할 수 있다.
또한 fetch 함수의 다양한 결과를 제어해서 테스트의 다양한 시나리오들을 시험해볼 수 있다.
패키지 버전 13.0.0은 새 createRootAPI에 대한 지원을 추가함.
프로젝트의 루트 디렉터리에서 터미널을 열고 다음 명령을 실행
npm install @testing-library/react@latest
npm install @testing-library/jest-dom@latest
npm install @testing-library/user-event@latest
index.js 파일은 새 createRootAPI를 사용하여 애플리케이션을 렌더링해야 함.
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<App />);
이 또한 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();
});