ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [CS Study] 프론트엔드 테스트 코드란?
    네트워크/CS Study 2024. 3. 19. 14:36
    개요
    1. 왜 해야할까?
    2. 테스트 코드 종류
    3. Jest and React Testing Library

     

     

    ✅ 왜 해야할까?


    프론트엔드 개발 환경의 발전에 따라 요구하는 애플리케이션 수준이 복잡해지고 다양한 방법론과 도구들이 등장하게 되었다. 이 중에서 효율적으로 테스트를 할 수 있는 방법과, 테스트를 해야하는 이유 및 개념에 대해서 알아보도록 하자.

     

    📦 테스트란?

    "프로그램을 실행하여 오류와 결함을 검출하고 애플리케이션이 요구사항에 맞게 동작하는지 검증하는 절차"로써 발생 가능한 결함을 예방하고 개발 과정에서 생기는 변경 사항으로 인해 새로운 결함이 생기는지 확인하여 자신감 있게 리팩토링 및 코드 개선을 할 수 있으며, 자동화를 통해 휴먼 에러의 가능성도 낮출 수 있다.
    • 빠르고 신회할 수 있는 테스트를 수행함으로 인해 QA와의 불필요한 커뮤니케이션을 줄이고 궁극적으로 비용의 감소로 이어진다.
    • 모든 상황에서 테스트를 하는 것은 어렵기 때문에 효율적으로 테스트를 하는 방법을 고민하고 실행해야 한다.

     

    📦 테스트 코드의 필요성

    1. 자동화를 통해 휴먼 에러를 낮춰 신뢰성이 높은 코드를 작성할 수 있게 해준다.
    2. 테스트를 통해 내가 작성한 코드가 의도대로 작성한다는 것을 보장하기에 유지 보수 및 수정시에 더욱 큰 역할을 한다.
    3. 사이드 이펙트를 파악하기 쉽고, 기술부채를 막을 수 있으며 이를 통해 프로젝트의 수명 연장 및 코드의 중복을 막을 수 있다.

     

    📦 TDD

    테스트 주도 개발의 약자로 개발하기 전 테스트를 진행하여 요구사항을 만족하는지 확인할 수 있으며 이를 통해 안정성있는 코드 및 코드에 대한 확신을 가질 수 있게끔하는 소프트웨어 개발 방법론이다

     


     

    ✅ 테스트 종류


    테스트 피라미드 (Martin Fowler, Kent.C.Dodds)

     

    📦 정적 테스트 (Static)

    코드를 실행시키지 않고 테스트를 실행하는 것으로 Type Error나 Reference Error와 같이 개발자의 실수로 인해 발생하는 에러를 방지할 수 있다.
    • Typescrpit, ESlint

     

    📦 단위 테스트 (Unit)

    각 모듈을 단독 실행 환경에서 독립적으로 테스트하는 것을 말하며 특정 컴포넌트를 랜더링해서 깨지지 않는지 확인하는 것을 예로 들 수 있다. 단위 테스트는 Sociable 테스트 Solitary 테스트가 있는데 Sociable 테스트는 자식 컴포넌트까지 포함해서 랜더링하는 것, Solitary 테스트는 자식 컴포넌트를 모킹해서 렌더링 하는 것으로 볼 수 있다.
    • Jest, Testing Library

    🎉 코드 예시)

    더보기
    import { render } from '@testing-library/react';
    import Item from '../components/Item';
    
    describe('Item', () => {
      it('should render item name and price', () => {
        const { getByText } = render(
          <Item name="Test Item" price={12000} addToCart={() => {}} />
        );
        expect(getByText('Test Item')).toBeInTheDocument();
        expect(getByText('Price: 12000')).toBeInTheDocument();
      });
      
      it('should remove item stock', () => {
      // ... 
      });
      // ...
    });

     

    📦 통합 테스트 (Integration)

    두 개 이상의 모듈이 실제로 연결된 상태를 테스트 하는 것으로 모듈 간의 연결에서 발생할 수 있는 에러를 검증할 수 있으며, 단위 테스트보다 비교적 넓은 범위에서 테스트하기에 리팩토링에 쉽게 깨지지 않는다. UI와 API 간의 상호작용이 올바르게 일어나는지 또는 state에 따라 UI의 변경이 올바르게 동작하는지 확인해 보는 것을 예시로 들 수 있다. 
    • 단위 테스트와 비슷한 개념의 broad 테스트와 narrow 테스트가 있으며 broad 테스트의 경우 실제 API를 호출하는 방식 narrow 테스트는 모킹, 가상 API를 이용하는 방식이다.
    • Jest, Testing Library

    🎉 코드 예시)

    더보기
    import { render, screen, fireEvent } from '@testing-library/react';
    import Shop from '../pages/shop';
    
    describe('Shop', () => {
      it('should display receipt after checkout', () => {
        render(<Shop />);
        const item1Button = screen.getByText('Item 1');
        const item2Button = screen.getByText('Item 2');
        const cartItems = screen.getAllByTestId('cart-item');
        const checkoutButton = screen.getByText('Checkout');
        const receiptItems = screen.getAllByTestId('receipt-item');
        const total = screen.getByTestId('receipt-total');
    
        // 장바구니에 상품 추가
        fireEvent.click(item1Button);
        fireEvent.click(item2Button);
        expect(cartItems).toHaveLength(2);
    
        // 장바구니에서 상품 제거
        const removeButton = cartItems[0].querySelector('button');
        fireEvent.click(removeButton);
        expect(cartItems).toHaveLength(1);
    
        // 상품을 구매하고, 영수증 출력
        fireEvent.click(checkoutButton);
        expect(receiptItems).toHaveLength(1);
        expect(receiptItems[0]).toHaveTextContent('Item 2 - 12000원');
        expect(total).toHaveTextContent('Total: 16000원');
      });
    });

     

    📦 E2E 테스트 (End to End)

    실제 사용자 입장 및 환경에서 테스트하는 것을 말하며, 주로 유저 스토리 기반의 시나리오를 통해 테스트를 진행한다. 시스템이 예상대로 작동하고 사용자의 요구 사항을 충족하는지 확인하기 위해 모든 구성 요소와 해당 구성 요소의 상호 작용을 테스트하는 것을 포함한다.
    • 브라우저를 띄워 진행되기에 실행 속도가 느리고, 실행 환경에 의한 문제(네트워크 에러 등)로 인해 테스트가 실패할 수 있어 신뢰도가 상대적으로 낮다.
    • 실제 상황에서 발생할 수 있는 에러를 검출할 수 있다.
    • Cypress, Puppeteer

    🎉 코드 예시)

    더보기
    const puppeteer = require('puppeteer');
    
    describe('Shopping', () => {
      let browser;
      let page;
    
      beforeAll(async () => {
        // 테스트 환경 설정
        browser = await puppeteer.launch();
        page = await browser.newPage();
        await page.goto('http://localhost:3000');
        await page.type('[data-testid="login-email"]', 'user@example.com');
        await page.type('[data-testid="login-password"]', 'password');
        await page.click('[data-testid="login-button"]');
        await page.waitForNavigation();
      });
    
      afterAll(async () => {
        // 브라우저 종료
        await browser.close();
      });
    
      it('should allow a user to buy an item and receive a receipt', async () => {
        // 상품을 장바구니에 추가
        await page.click('[data-testid="item-123"]');
        await page.click('[data-testid="add-to-cart-button"]');
        await page.click('[data-testid="cart-icon"]');
    
        // 장바구니의 추가된 상품 체크
        const cartItem = await page.waitForSelector('[data-testid="cart-item"]');
        expect(await cartItem.evaluate(el => el.innerText)).toContain('Item 1');
    
        // 아이템 구매 
        await page.click('[data-testid="checkout-button"]');
        await page.click('[data-testid="confirm-purchase-button"]');
        await page.waitForSelector('[data-testid="receipt"]');
    
        // 영수증 화면 출력
        const receiptItems = await page.$eval('[data-testid="receipt-items"]', el => el.innerText);
        expect(receiptItems).toContain('Item 1');
        expect(receiptItems).toContain('12000 원');
        const receiptTotal = await page.$eval('[data-testid="receipt-total"]', el => el.innerText);
        expect(receiptTotal).toContain('Total: 16000 원');
    
        // 영수증을 고객 메일로 전송
        await page.click('[data-testid="send-receipt-button"]');
        const receiptSent = await page.waitForSelector('[data-testid="receipt-sent"]');
        expect(await receiptSent.evaluate(el => el.innerText)).toContain('Receipt sent to user@example.com');
      });
    });

     


     

    ✅ Jest and Storybook


    앞서 테스트에 대한 개념 및 종류에 대해 알아보았습니다. 이제는 우리가 직접 테스트에 사용할 Jest와 Testing Library의 기본적인 사용방법에 대해 알아보겠습니다.

     

    📦 Jest

    페이스북에서 개발한 테스팅 프레임워크로 Test Runner, Matcher, Mock 각각 달리 사용하던 이전과는 다르게 Jest를 통해 모두 해결할 수 있게 되었다.
    • 직관적이며 문서화가 잘 되어있다.
    • 여러 라이브러리의 조합 없이 편리하게 코드를 테스트할 수 있다.

     

    기본문법

    describe('계산 테스트', () => {
       const a = 1, b = 2;
    
       test('a + b는 3이다.', () => {
          expect(a + b).toEqual(3);
       });
    });
    
    /*
    describe('그룹 테스트 설명 문자열', () => {
       const a = 1, b = 2; // 테스트에 사용할 일회용 가짜 변수 선언
    
       test('개별 테스트 설명 문자열', () => {
          expect(검증대상).toXxx(기대결과);
       });
    });
    */

    파일명: 테스트할함수파일명.test.ts

    describe: 테스트를 묶는 그룹

    beforeEach: 테스트 이전에 실행하는 부분으로 test 실행 전 매번 실행되며 초기화를 반복

    test: 하나의 테스트 단위로 실질적으로 테스트하고자 하는 내용 (Matcher 등등 여기 쓰임) 

     

    다양한 기능

    • async / await 뿐만아니라 콜백함수 등 비동기 코드 테스트가 가능하다
    • mock func를 통해서 예상대로 호출되는지 쉽게 확인할 수 있다.
    • 다양한 커뮤니티와 가이드를 통해서 Jest의 더 많은 기능을 찾아보기 쉽다.

     

    📦 Storybook

    오늘날 디자이너와 개발자들은 UI 컴포넌트 구조화를 위해 다양한 노력을 하고 있다. 이를 위한 방법으로 디자인 시스템이 있는데 이는 재사용 가능한 UI를 구성하여 사용자가 접근하기 쉬운 UI를 구축하고 디자이너와 개발자 사이를 연결하는 다리 역할을 하기도 한다. 그리고 이러한 디자인 시스템을 구축하기 위한 빌드 컴포넌트(UI 컴포넌트 개발 및 자동 문서화)로 사용되는 것이 스토리북입니다. 그렇기에 3가지 기술적 파트에 대해 알아보고 어떻게 디자인 시스템을 구축할 것인지에 대해 알아보겠습니다.
    • 재사용이 가능한 공용 UI 컴포넌트
    • 디자인 토큰: 브랜드 색상, 간격과 같은 스타일 변수
    • 문서: 사용 방법, 설명, 예시

     

     

    작업흐름

    스토리북에서 권장하는 작업 흐름은 다음과 같다.

    1. UI 컴포넌트 구축
    2. 리뷰를 통한 피드백 수집 및 UI 버그 테스트
    3. 문서화
    4. 프로젝트에 적용

    storybook은 기본적으로 Setup Wizard가 잘 되어있기에 사용하는데 크게 어렵지 않으며 docs를 보면서 기본적으로 어떻게 사용할 지에 대해서 파악하기 쉽게 되어있다. 그렇기에 디자인 시스템과 관련되서 현재 프로젝트에 어떻게 적용할지를 고민하여 적용하면 될 것같다.

     

     

     

     

     

    📌 reference

     

    댓글

Designed by Tistory.