유튜브 강의 링크 - React Testing Crash Course
프로젝트의 목표와 할당된 시간, 예산에 따라 테스트를 해야하는 대상이 달라지고 테스트 우선 순위가 달라지는 것이 당연하다.
하지만 강의자는 다음 우선 순위가 잘 맞았다고 한다.
테스트에도 3가지 종류가 있다. 단위 테스트(Unit Test), 통합 테스트(Integration Test), E2E(End to End)가 있다.
단위 테스트는 하나의 모듈을 기준으로 독립적으로 테스트 하는 것이다. 모듈은 하나의 함수 혹은 메소드를 각각 테스트를 하는 것이다. 예를 들어, 더하기 함수 function add(a, b){}
같이 가장 작은 단위를 테스팅 하는 것을 의미한다.
통합 테스트는 여러 개의 단위 테스트(모듈)를 모아서 더 큰 단위의 기능이 정상적으로 작동 되는지 확인하는 테스트이다. 예를 들어 로그인 기능을 테스트 할 때 아이디, 비밀번호의 validation에 대한 단위 테스트, 아이디, 비밀번호를 입력 후 로그인 버튼을 눌렀을 때 정상적으로 로그인이 되는 단위 테스트를 하나의 테스트로 묶어서 테스트 하는 것이 있다.
위의 이미지는 단위 테스트와 통합 테스트의 차이를 보여주는 이미지인 것 같다.프론트엔드에서의 E2E 테스트는 사용자의 입장이 되어 실제 웹페이지에서 원하는 행동이 나오는지를 테스트하는 것이다. 예를 들어, 로그인 화면에 아이디, 비밀번호를 입력하고, 로그인 버튼 눌렀을 때 로그인이 되어 있는 상태에서 메인 화면으로 이동으로 되는지를 테스트 하는 것이다.
리액트에서 단위 테스트, 통합 테스트를 할 때 주로 react-testing-library
를 사용한다. react-testing-library
를 많이 쓰는 이유에는 Enzyme
라는 테스트 라이브러리와 비교해야 한다.
Enzyme
는 컴포넌트 상태 변화 이전과 이후의 props, state의 변화를 비교하는 테스트라 props와 state를 조회해야 해서 테스트 코드를 작성에 복잡함이 추가된다.
반면, react-testing-library
는 props, state 값의 변화보다는 사용자의 관점에서 컴포넌트의 동작/행위에 초점을 두는 라이브러리라서 테스트 코드를 작성할 때 좀 더 직관적으로 생각하면서 작성할 수 있는 것이 장점이다.
다음은 작은 단위(모듈 단위)로 테스트 코드를 작성한 단위 테스트 코드이다.
모듈 1: Pay 버튼은 비활성화
모듈 2: 조건에 부합되면 Pay 버튼은 활성화
test('모듈 1 - 처음 페이지를 렌더링할 때, Pay 버튼은 비활성화 되어 있어야 한다', async () => {
render(<TransactionCreateStepTwo sender={{ id: '5' }} receiver={{ id: '5' }} />);
expect(screen.getByRole('button', { name: /pay/i }).toBeDisabled();
});
test('모듈 2 - amount와 note가 입력이 되었으면, Pay 버튼은 활성화 되어 있어야 한다', async () => {
// Arrange
render(<TransactionCreateStepTwo sender={{ id: '5' }} receiver={{ id: '5' }} />);
// Act
userEvent.type(screen.getByPlaceholderText(/amount/i, "50");
userEvent.type(screen.getByPlaceholderText(/add a note/i, "dinner");
// Assert
expect(screen.getByRole('button', { name: /pay/i }).toBeEnabled();
});
단위 테스트를 작성하기 위한 3단계(Three Test Phase)
Arrange - 테스트를 판을 깔아주는 단계. 예를 들어 테스트하고 싶은 컴포넌트를 렌더링부터 먼저 해야 한다.
Act - 유저가 직접 이벤트를 실행하는 단계 (User clicking button, User typing email and password)
Assert - 유저가 어떤 이벤트를 하고 나서 결과가 예상되는 결과와 일치하는지를 검증하는 단계
Pay 페이지 시나리오는 Amount, Add a note 인풋을 입력 안하면 Pay 버튼은 비활성화 되고 Amount 인풋을 입력하고 Add a note 인풋을 입력을 하고 나면 Pay 버튼이 활성화 되는 것이다.
결국은, 위 두 개의 단위 테스트들은 Pay 페이지 시나리오에서 바라봤을 때 하나의 테스트로 합쳐도 된다.
test('모듈 1 - 처음 페이지를 렌더링할 때, Pay 버튼은 비활성화 되어 있어야 하고, amount와 note가 입력이 되어 있을 때 pay 버튼은 활성화 되어있어야 한다', async () => {
render(<TransactionCreateStepTwo sender={{ id: '5' }} receiver={{ id: '5' }} />);
expect(screen.getByRole('button', { name: /pay/i }).toBeDisabled();
userEvent.type(screen.getByPlaceholderText(/amount/i, "50");
userEvent.type(screen.getByPlaceholderText(/add a note/i, "dinner");
expect(screen.getByRole('button', { name: /pay/i }).toBeEnabled();
});
이처럼, 단위 테스트들을 작성하다가 테스트 시나리오 상 하나로 합쳐도 된다는 생각이 들면 같은 테스트 단위에 넣으면 된다.
리액트에서 E2E 테스트는 주로 cypress
를 이용한다.
다음은 결제하는 과정을 cypress로 작성한 코드이다
const { v4: uuidv4 } = require('uuid');
describe('payment', () => {
it('user can make payment', () => {
// login
cy.visit('/');
cy.findByRole('textbox', { name: /username/i }).type('johndoe');
cy.findByLabelText(/password/i).type('s3cret');
cy.findByRole('checkbox', { name: /remember me/i }).check();
cy.findByRole('button', { name: /sign in/i }).click();
// check account balance
let oldBalance;
cy.get('[data-test=sidenav-user-balance]').then($balance => oldBalance = $balance.text());
// click on new button
cy.findByRole('button', { name: /new/i }).click();
// search for user
cy.findByRole('textbox').type('devon becker');
cy.findByText(/devon becker/i).click();
// add amount and note and click pay
const paymentAmount = "5.00";
cy.findByPlaceholderText(/amount/i).type(paymentAmount);
const note = uuidv4();
cy.findByPlaceholderText(/add a note/i).type(note);
cy.findByRole('button', { name: /pay/i }).click();
// return to transactions
cy.findByRole('button', { name: /return to transactions/i }).click();
// go to personal payments
cy.findByRole('tab', { name: /mine/i }).click();
// click on payment
cy.findByText(note).click({ force: true });
// verify if payment was made
cy.findByText(`-$${paymentAmount}`).should('be.visible');
cy.findByText(note).should('be.visible');
// verify if payment amount was deducted
cy.get('[data-test=sidenav-user-balance]').then($balance => {
const convertedOldBalance = parseFloat(oldBalance.replace(/\$|,/g, ""));
const convertedNewBalance = parseFloat($balance.text().replace(/\$|,/g, ""));
expect(convertedOldBalance - convertedNewBalance).to.equal(parseFloat(paymentAmount));
});
});
});
개인적인 생각으로 react-testing-library
와 cypress
둘 다 사용자 관점에서 컴포넌트를 테스트하는 것이라 한 프로젝트에 둘 다 쓰기에는 중복되는 점이 너무 많다.
회사에서의 프로젝트들은 고유의 규칙, 철학에 따라 둘 다 쓰거나, 둘 중 하나를 선택하겠지만, 내 개인 프로젝트에서는 리액트 컴포넌트를 테스트하는 목적에서는 둘 중에 하나만 선택해서 쓸 생각이다.
왜냐하면 프로덕션 코드를 서포트 하기 위해 테스트 코드를 작성하는 것이지 사용자 관점에서 컴포넌트를 테스트하는 측면에서 중복되는 지점이 많은 두 테스트 라이브러리를 시간을 낭비하면서 동시에 쓸 필요가 없기 때문이다.
출처
Edge Case - https://daryeou.tistory.com/203
단위 테스트 - https://mangkyu.tistory.com/143