Knowledge/Test

프론트엔드 테스트 - TDD와 종류(Unit, Integration, E2E)

SooJae 2023. 2. 27. 00:45
반응형

 

 

TDD로고

 

테스트 주도 개발(TDD)이란?

TDD는 Test-Driven Development의 약자로 실제 코드를 작성하기 전에 테스트 코드를 먼저 작성하는 개발 프로세스다. TDD 프로세스는 다음 단계를 따른다.

  1. 테스트 코드 작성: 우선 구현하려는 동작을 설명하는 테스트 코드를 작성한다. 이때, 아직 동작을 구현하는 코드를 작성하지 않았으므로 테스트는 실패 해야한다. 
  2. 테스트 실행: 테스트를 실행한다. 위에 언급했듯이, 코드를 작성하지 않았으므로 실패해야한다.
  3. 코드 작성: 1번에 작성했던 설명에 부합하는 코드를 작성한다. 이때 테스트를 통과하기 위한 목적으로만 작성해야한다.
  4. 즉 아직 존재하지 않은 많은 문제들을 생각하여 코드를 작성하기보다는 테스트 목표에 집중하여 작성하는 것이 중요하다.
  5. 테스트 실행: 테스트를 다시 실행한다.
  6. 리팩터링: 테스트를 통과하면 코드를 리팩터링 한다.  

 

TDD의 이점

  • 테스트를 먼저 작성함으로써, 개발자는 구현하려는 동작에 대해 먼저 생각하게 되어 특정한 요구사항에 맞는 코드를 작성할 수 있다.
  • 코드의 유지보수성을 향상시키고, 시간이 지나도 프로젝트의 유지 관리가 쉬워진다.
  • 코드 수정 시 미처 생각지 못한 부분(기존 코드에 영향을 받는)에 대한 문제점을 미리 파악할 수 있다.
  • 애플리케이션에 대한 신뢰성이 높아진다.
  • 테스트 코드는 동작 방식을 설명하니, 테스트 파일 자체를 문서로 활용할 수  있다.
  • 각종 에러 케이스에 대한 생각을 많이 하게 되므로 두뇌 건강에 좋다.

 

테스트 종류

 

Unit vs Integration Test
잠금(Unit) 테스트는 통과. 잠금과 상호 작용하는 문(Integration) 테스트는 실패

 

 

단위 테스트(Unit Test)

툴: Jasmine, Jest, Karma, Mocha

  • 단위 테스트는 함수나 메서드와 같은 작은 단위의 코드를 테스트하는 데 중점을 두는 테스트 유형이다. 즉, 시스템의 전체적인 동작에 중점을 맞추지는 않는다.
  • 단위 테스트의 목적은 코드의 각 단위가 예상대로 작동하고 요구 사항을 충족하는지 확인하는 것이다.

예: 카카오 프렌즈샵에서 상품을 구매하는 경우, 단위 테스트는 `구매` 기능을 테스트할 때 고객의 잔액에서 상품의 금액 만큼 출금하는지, 상품의 재고를 업데이트 하는지, 영수증을 잘 생성하는지 각각 확인한다.
 
코드 예시 

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 Test)

툴 종류: 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 Test)

툴 종류: Cypress, Puppeteer

  • E2E 테스트는 시스템의 시작부터 끝까지 전체 흐름을 확인하는 테스트 유형이다.
  • 시스템이 예상대로 작동하고 사용자의 요구 사항을 충족하는지 확인하기 위해 모든 구성 요소와 해당 구성 요소의 상호 작용을 테스트하는 것이 포함된다.

예: 카카오 프렌즈샵에서 상품을 `구매`하는 경우

  1. 고객이 카카오 로그인
  2. 상품을 선택하여 장바구니에 추가
  3. 상품 구매
  4. 결제 방식 선택
  5. 구매 완료
  6. 구매 영수증을 고객 메일로 전송

위의 예시처럼 사용자의 입장에서 전체 Flow가 정상적으로 동작을 하는지 확인한다. 
 
코드 예시

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');
  });
});

 
위에서 사용한 테스팅 라이브러리인 Puppeteer와 Jest에 대해 좀 더 알아보고 싶다면, 아래 포스트들을 읽어보는 것을 추천한다.

Puppeteer

 

[test tool 1] Puppeteer란?

소개 puppeteer은 구글에서 만든 노드 라이브러리로 Headless Chrome 또는 Chrominum을 제어할 수 있다. * headless? 백그라운드에서 작동하는 브라우저이다. 때문에 일반 사용자가 브라우저를 사용할 때처럼

kkangdda.tistory.com

 

 

Jest

 

Jest Tutorial: A Comprehensive Guide With Examples And Best Practices

This tutorial dive deep into every aspect of the Jest framework (i.e., installation, configuration, etc.) and test execution on local & cloud grids. The learnings of this Jest tutorial can be utilized in using Jest in an ideal manner in your project

www.lambdatest.com

 


그냥 E2E로만 테스트하면 안 될까?

위의 코드를 보면 어차피 Unit, Integration 테스트 코드를 작성해도, 실제로 사용자는 E2E 방식으로 동작할텐데 굳이 시간 써가면서 각 코드를 써야할까라는 생각이 든다. 
사실 Unit, Integration에서 테스트 했던 내용들은 E2E 테스트로 확인할 수 있고, 테스트가 중복되는 경우가 많다.

하지만, Unit이 E2E보다 테스트 실행 시간이 훨씬 빠르다. 그리고 세분화 할수록 어떤 에러가 발생했는지도 빠르게 체크 할 수 있다. 정리하면 다음과 같다.
 
E2E만 사용할 경우(granularity가 낮을 수록)

  • 테스트 코드 작성 시간 ↑
  • 유지보수 비용 ↑
  • 이슈 발생 시, 문제 해결 시간 ↓

 

E2E, Integrationm, Unit을 사용할 경우(granularity가 높을 수록)

  • 테스트 작성 시간 ↓
  • 유지보수 비용 ↓
  • 이슈 발생 시, 문제 해결 시간 ↑

어떤 방식이 특별히 나쁘다라고 단언할 수는 없다. 각각의 Trade Off가 존재한다. 어느 정도까지 세분화를 할지는 프로젝트 규모와 개발 기한에 따라 달라질 것이다.
 

 

간단 팁

  • 필요없는 테스트는 넘어가도 된다: 예를 들어 데이터베이스를 연결이 잘 되는지는 프론트엔드와 관련이 없으므로 테스트할 필요가 없다. 이에 대한 것은 따로 Unit 테스트를 할 필요가 없다.
  • E2E 테스트가 가장 우선시 되야한다: 결국은 사용자 관점으로 봐야하므로 E2E 테스트가 가장 우선시 되야한다. 하지만 동작 Flow와 관련 없는 것들은 세분화하자 (예: 프렌즈 샵의 상품의 재고를 표시하는 것은 굳이 E2E 테스트에서 확인할 필요가 없다.)
  • 함수의 기능이 `단순한 연결`일 경우는 작성하지 않아도 된다: 예를 들어 NextJS의 getStaticProps는 데이터를 갖고와서 props에 넘겨주기만 하는 함수이다. 이 함수는 테스트할 필요가 없다. (함수 내에 데이터를 변환하거나 복잡한 로직이 있으면 모를까)
반응형