React Testing Crash Course 유튜브 강의 정리

Sungmin·2023년 12월 14일
0
post-thumbnail

유튜브 강의 링크 - React Testing Crash Course

리액트 테스팅 관련 강의를 보게된 동기

  • 테스팅 코드가 거의 없는(커버리지가 10%도 안되었다) 회사 솔루션에 새로운 기능을 넣을 때마다 기존 기능에서 발생하는 예상치 못한 에러, 버그의 원인을 찾고 고치는데 정말 힘이 많이 들었다.
    - QA 팀에서 하나씩 입력하고 눌러가면서 테스트를 할 때 처음에는 잘 작동 되었던 기능이 새로운 기능을 추가하고 나서부터는 작동이 잘 안되었던 기억이 있다.
  • 테스트 코드를 작성한다는 것이 중요하다는 것을 인지하면서도 정작 리액트 테스팅 코드를 어떻게 작성하는지 하나도 몰랐다.

Why should you test?

  • 우리가 만든 애플리케이션이 예상했던 대로 작동이 되는지 체크하기 위해
  • 새로운 기능을 넣을 때마다 기존 기능에서 발생하는 예상치 못한 에러, 버그 원인을 바로 알아내려고 -> 이거는 너무나 공감한다. 테스트 코드 작성 안하게 되면 에러, 버그 원인 찾는데 너무나 많은 시간을 낭비하게 된다.

Why do I have Test Priorities?

  • 프로젝트에 시간과 돈이 할당된 만큼만 소비 되어야 하기 때문이다.
    예를 들어, 자동차 손 세차장에 왔다고 하자. 당장 돈은 2000원 밖에 없는데 내일 당장 자동차를 끌고 클라이언트와의 첫 미팅을 가야한다. 이렇게 주어진 시간과 돈이 부족할 때 자동차를 세차를 해야하는 부분은 차 유리랑 차체를 먼저 닦는 것이 급선무 일 것이다.

What should I test?(Test Priorities)

프로젝트의 목표와 할당된 시간, 예산에 따라 테스트를 해야하는 대상이 달라지고 테스트 우선 순위가 달라지는 것이 당연하다.
하지만 강의자는 다음 우선 순위가 잘 맞았다고 한다.

  1. 프로젝트에서 회사의 수익이 가장 많이 나는 기능들 (High value features)
    • Amazon은 수익이 나기 위해 필수적인 기능은 상품들의 정보 페이지 그리고 결제 기능이다.
    • Spotify는 음악을 재생하는 기능과 구독 서비스 기능이 가장 중요하다.
  2. 극한의 상황에서도, 즉 최대/최소의 값을 넣어도 서비스 정상 작동 여부(Edge cases in high value features)
    • Edge case는 극한의 상황을 테스트하는 케이스이다.
    • 예를 들어, 이커머스의 상품 100만개를 만들어 본래 기능이 정상 작동하는지 테스트한다.
  3. 자주 망가지는 기능들(Things that are easy to break)
  4. 기본적인 리액트 컴포넌트 테스팅 (Basic React component testing)
    • User interactions
    • Conditional rendering
    • Utils/Hooks

Types of Testing

테스트에도 3가지 종류가 있다. 단위 테스트(Unit Test), 통합 테스트(Integration Test), E2E(End to End)가 있다.

단위 테스트(Unit Test)

단위 테스트는 하나의 모듈을 기준으로 독립적으로 테스트 하는 것이다. 모듈은 하나의 함수 혹은 메소드를 각각 테스트를 하는 것이다. 예를 들어, 더하기 함수 function add(a, b){} 같이 가장 작은 단위를 테스팅 하는 것을 의미한다.

통합 테스트(Integration Test)

통합 테스트는 여러 개의 단위 테스트(모듈)를 모아서 더 큰 단위의 기능이 정상적으로 작동 되는지 확인하는 테스트이다. 예를 들어 로그인 기능을 테스트 할 때 아이디, 비밀번호의 validation에 대한 단위 테스트, 아이디, 비밀번호를 입력 후 로그인 버튼을 눌렀을 때 정상적으로 로그인이 되는 단위 테스트를 하나의 테스트로 묶어서 테스트 하는 것이 있다.

위의 이미지는 단위 테스트와 통합 테스트의 차이를 보여주는 이미지인 것 같다.

E2E 테스트 (End to End test)

프론트엔드에서의 E2E 테스트는 사용자의 입장이 되어 실제 웹페이지에서 원하는 행동이 나오는지를 테스트하는 것이다. 예를 들어, 로그인 화면에 아이디, 비밀번호를 입력하고, 로그인 버튼 눌렀을 때 로그인이 되어 있는 상태에서 메인 화면으로 이동으로 되는지를 테스트 하는 것이다.


단위 테스트, 통합 테스트, E2E 코드 작성

단위 테스트

리액트에서 단위 테스트, 통합 테스트를 할 때 주로 react-testing-library를 사용한다. react-testing-library를 많이 쓰는 이유에는 Enzyme라는 테스트 라이브러리와 비교해야 한다.

Enzyme vs react-testing-library

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 테스트

리액트에서 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-librarycypress 둘 다 사용자 관점에서 컴포넌트를 테스트하는 것이라 한 프로젝트에 둘 다 쓰기에는 중복되는 점이 너무 많다.

회사에서의 프로젝트들은 고유의 규칙, 철학에 따라 둘 다 쓰거나, 둘 중 하나를 선택하겠지만, 내 개인 프로젝트에서는 리액트 컴포넌트를 테스트하는 목적에서는 둘 중에 하나만 선택해서 쓸 생각이다.

왜냐하면 프로덕션 코드를 서포트 하기 위해 테스트 코드를 작성하는 것이지 사용자 관점에서 컴포넌트를 테스트하는 측면에서 중복되는 지점이 많은 두 테스트 라이브러리를 시간을 낭비하면서 동시에 쓸 필요가 없기 때문이다.

출처
Edge Case - https://daryeou.tistory.com/203
단위 테스트 - https://mangkyu.tistory.com/143

profile
Share everything that I like

0개의 댓글