Test Driven Development

speciaLLunch·2023년 3월 3일
0

TestDrivenDevelopment

목록 보기
1/5

회사의 기존 제품에 테스트 프레임워크 및 라이브러리를 도입하고 CI/CD 자동화까지 하는 프로젝트를 시작하였다. 먼저 TDD 테스트 주도 개발에 대해 알아보고 테스트 종류에는 어떤 것들이 있는지, 어떤 프레임워크/라이브러리가 도입하기 좋고 범용적으로 쓰이는지 조사해보았다.


TDD

  • Test Driven Development : 테스트 주도 개발
  • 테스트 종류
    • 정적(Static) 테스트
      • 구문 오류, 나쁜 코드 스타일 등을 검증
      • ex. ESLint, Typescript
    • 유닛(Unit) 테스트
      • 클래스/함수, 컴포넌트 단위로 테스트
      • 의존성 있는 컴포넌트들은 Mocking 필요
      • ex. 리덕스의 액션 생성 함수가 액션 객체를 잘 만들어낸다
      • ex. 리덕스의 리듀서에 상태와 액션객체를 넣어서 호출하면 새로운 상태를 잘 만들어준다
      • ex. jest
    • 통합(Integrated) 테스트
      • 여러 컴포넌트들을 렌더링하고 서로의 상호 작용에 대한 테스트
      • 큰 규모의 기능이나 페이지가 제대로 동작 하는지에 대한 테스트
      • ex. DOM 이벤트를 발생 시켰을 때 우리의 UI 에 원하는 변화가 잘 발생하는지 테스트
      • ex. 리덕스와 연동된 컨테이너 컴포넌트의 DOM 에 특정 이벤트를 발생시켰을 때 우리가 원하는 액션이 잘 디스패치 되는지 테스트
      • ex. jest, RTL, enzyme
    • E2E 테스트
      - 실제 사용자가 사용하는 것과 같은 조건에서 전체 시스템을 테스트
      - API 서버, DB 등의 외부 서비스들을 모두 사용하여 통합된 시스템을 테스트
      - 비용이 많이들고 속도도 느림
      - 네트워크 에러 등 외부 요인으로 인해 테스트가 실패할 가능성도 있음
      - ex.Cypress, Selenium

프론트엔드 테스팅 대상

  1. 사용자 이벤트 처리
    • 프론트 엔드에서 이벤트는 이벤트 핸들러로 처리됨
    • 이벤트 테스트시에는 테스트 유틸리티를 활용하여 이벤트를 시뮬레이션하거나, E2E 테스트에서 실제로 테스트를 브라우저에서 이벤트를 발생시켜 테스트 해야 함
    • ex. RTL
// RTL
import { fireEvent } from '@testing-library/react';

fireEvent.change(검색창, { target: { value: '테스트 방법' } });
fireEvent.click(검색버튼);

// cypress
it('greets', () => {
  cy.visit('app.html')
  cy.get('#name').type('Cypress{enter}')
  cy.contains('#answer', 'Cypress')
})
  1. API 서버 통신
    • 실제 API 서버 이용하는 방법
    • mock API 서버 이용하는 방법
const response = await fetch('https://api.com', {
  method: 'POST',
  body: JSON.stringify({
    혼인: {
      남편: '크리스',
      아내: '피터'
    },
  }),
});
const 결과 = await response.json();

expect(결과).toBe("결혼");

// mocking
jest.spyOn(window, 'fetch');  // fetch 함수 mocking
window.fetch.mockImplementation({
  json: async () => '결혼',
});
  1. 시각적 요소
    • 프론트엔드에서는 서버와 달리 입력 값이 사용자로부터 나오고(마우스,키보드), 출력은 시각적 요소(모니터)이다. 따라서 시각적인 부분을 코드로 테스트하는 부분이 필요
    • 스냅샷 테스트
      • HTML 구조가 의도한대로 나타나는지 테스트. 이전에 저장한 스냅샷과 HTML 구조를 비교
      • ex. Jest의 toMatchSnapshot()
    • 시각적 회귀 테스트
      • HTML에 CSS까지 더해서 컴포넌트가 실제로 브라우저에 렌더링되는 모습이 의도한대로 나타나는지 테스트
      • ex. StoryBook의 Chromatic

테스트 환경

  • Browser 환경
    • 프론트엔드에서 브라우저 호환성이나 기기 호환성에 대한 테스트도 필요할 수 있음
    • Web API도 사용하여 테스트 가능 장점
    • Node.js에 비해서는 속도가 느리고 브라우저가 실행되어야 하니 별도의 런처를 설치해야 함
    • ex. (E2E) Karma, Selenium, Cypress
  • Node.js 환경
    • 빠르긴하지만 브라우저 제공하는 web API나 DOM 접근 API를 사용못함
    • 이를 위해 jsdom처럼 DOM을 가상으로 구현하는 라이브러리를 사용할 수 있지만, page 네비게이션이나 레이아웃 등은 테스트 할 수 없다
  • 크로스 브라우징 테스트가 반드시 필요한 경우에만 브라우저 환경 사용을 추천
    • 왜냐면 브라우저 간 차이가 크게 없어지고 문법 호환성은 Babel이 잡아줘 더 빠르고 간결한 테스트 가능
    • DOM을 직접 조작하는 것도 React, Vue 등의 프레임워크가 대신 해준다
    • 또한 호환성 및 기기 테스트는 QA쪽에서 진행되므로
  • 추천조합
    • Broswer 환경: Karma + Jasmine
    • Node.js: Jest

단위테스트/통합테스트

  • Assertion
    • 테스트의 성공/실패를 판단하기 위한 조건을 표현
    • ex. assert.equal([1, 2, 3].indexOf(4), -1);
  • Moking & Stubbing
    • 모듈 별로 독립적으로 테스트하는 단위 테스트에서, 다른 특정 모듈을 필요로 할 때 사용
  • 테스트 툴 종류(프레임워크)
    • Jest
      • Facebook이 개발한 테스트 프레임워크
      • 초기에 React를 사용한 웹 애플리케이션 JS 테스트 목적(현재 Angular, Vue, Node.js도 지원)
      • CRA에 기본적으로 탑재
      • 가장 많이 사용됨
    • Mocha
      - Node.js 테스트 프레임워크
      - API는 jest랑 비슷함(describe, it, beforeEach 등)
      - Assertion, Mocking, Stubbing 등의 라이브러리를 포함하지 않으므로 작동하기 위해서 다른 라이브러리의 설치 및 설정 작업이 필요
      - 주로 Assertion ⇒ Chai, Mocking ⇒ Sinon 라이브러리를 사용하는 게 대표적
      - 복잡한 Mocking, 다양한 확장이 필요한 경우 추천
    • Enzyme
      • AirBnb에서 만듦
      • Virtual DOM 테스트 목적의 테스트 라이브러리
      • Shallow rendering: 테스트 하고자 하는 컴포넌트만 골라서 사용가능 (부모, 자식 관계는 관계 없이)
      • testing implementation(internal state): 유저 의도보단 컴포넌트 state에 중점을 두고 테스트
      • jest-dom assertion은 Enzyme 래퍼 개체에서 작동하지 않음
      • React 18 버전 이후 지원 중단
        • 이미 대규모 프로젝트에서 Enzyme 레거시가 없는 한 추천하지 않음..이라는 의견. 심지어 enzyme to RTL 마이그레이션도 존재.
    • React-Testing-Library (RTL)
      - Virtual DOM 테스트 목적의 테스트 라이브러리
      - 브라우저 없이도 DOM 조작 가능
      - Jest와 마찬가지로 CRA시 기본 탑재
      - 부모, 자식 컴포넌트 모두 사용(less isolated)
      - testing behavior (what’s on the page): 유저 의도로 작동하는지 테스트.
      - 테스트가 실패했을 때 어디가 문제인지 확인하기 까다로움
      - 그러나 state 등은 변경이 자주 될 수 있기에 test 구조 유지하는 데에 더 유리
    • 전반적으로 Enzyme과 RTL은 모두 React 구성 요소를 테스트하기 위한 강력한 도구를 제공하지만 접근 방식과 철학이 다르다
    • Enzyme은 구현 세부 사항을 테스트하는 데 더 중점을 두고 구성 요소 트리를 조작하기 위한 더 많은 옵션을 제공하는 반면 RTL은 구성 요소 동작을 테스트하고 구현 세부 사항을 테스트하지 않는 데 더 중점을 둡니다

Jest API

var message;
beforeEach(() => message = 'Vue');

test('message equals to be Vue', () => {
  expect(message).toBe('Vue');
});

test('message equals to be Vue!!', () => {
  expect(message + '!!').toBe('Vue');
});
  • describe()
    • 여러 개의 test() 코드를 하나의 테스트 작업 단위로 묶어주는 API
  • test()
    • 테스트 코드를 돌리기 위한 API. 하나의 테스트 케이스를 의미하며 it()과 같은 역할
  • expect()
    • 테스트 할 대상을 넣는 API. expect()에는 주로 테스트 입력 값 또는 기대 값을 넣습니다
  • toBe()
    • 테스트의 결과를 확인하는 API. 테스트의 예상 결과 값을 넣습니다
  • beforeEach()
    • 테스트 파일의 각 테스트 코드가 돌기 전에 수행할 로직을 넣는 API. 테스트 케이스마다 반복되는 로직을 넣기에 적합

테스트 패턴

1. Setup/Teardown
React component tree를 document의 DOM elements에 붙여 렌더링하여 테스트. 테스트 후에는 document로 부터 component tree를 언마운트하여 종료한다.

import { unmountComponentAtNode } from "react-dom";

let container = null;
beforeEach(() => {
  // DOM 엘리먼트를 렌더링 대상으로 설정
  container = document.createElement("div");
  document.body.appendChild(container);
});

afterEach(() => {
  // 종료시 정리
  unmountComponentAtNode(container);
  container.remove();
  container = null;
});

2. act()
리액트 react-dom/test-utils에서 제공
act 함수 안에 "unit"단위로 렌더링, 이벤트, 데이터 fetching을 진행한다. act 내부의 코드를 간결하게 하고 싶을 때 React Testing Library를 사용

3. 렌더링
컴포넌트 렌더링이 제대로 되었는지 테스트 예제

// hello.js
import React from "react";

export default function Hello(props) {
  if (props.name) {
    return <h1>Hello, {props.name}!</h1>;
  } else {
    return <span>Hey, stranger</span>;
  }
}
// hello.test.js

import React from "react";
import { render, unmountComponentAtNode } from "react-dom";
import { act } from "react-dom/test-utils";

import Hello from "./hello";

let container = null;
beforeEach(() => {

test(renders with or without a name", () => {
  act(() => {
    render(<Hello />, container);
  });
  expect(container.textContent).toBe("Hey, stranger");

  act(() => {
    render(<Hello name="Jenny" />, container);
  });
  expect(container.textContent).toBe("Hello, Jenny!");

  act(() => {
    render(<Hello name="Margaret" />, container);
  });
  expect(container.textContent).toBe("Hello, Margaret!");
});

4. Data Fetching
실제 API 호출하는 대신 dummy 데이터로 테스트. 아니면 end-to-end 프레임워크(Cypress, Playwright,Puppeteer)를 통해서 전체 app이 제대로 동작하는지 확인

// user.test.js

import React from "react";
import { render, unmountComponentAtNode } from "react-dom";
import { act } from "react-dom/test-utils";
import User from "./user";

let container = null;

test("renders user data", async () => {
  const fakeUser = {
    name: "Joni Baez",
    age: "32",
    address: "123, Charming Avenue"
  };
  jest.spyOn(global, "fetch").mockImplementation(() =>
    Promise.resolve({
      json: () => Promise.resolve(fakeUser)
    })
  );

  // resolved promises를 적용하려면 `act()`의 비동기 버전을 사용하세요.
  await act(async () => {
    render(<User id="123" />, container);
  });

  expect(container.querySelector("summary").textContent).toBe(fakeUser.name);
  expect(container.querySelector("strong").textContent).toBe(fakeUser.age);
  expect(container.textContent).toContain(fakeUser.address);

  // 테스트가 완전히 격리되도록 mock을 제거하세요.
  global.fetch.mockRestore();
});

5. Moking Modules
Data Fetching이랑 비슷하게 dummy 모듈 만들어서 테스트 할 수 있다.

// contact.test.js

import React from "react";
import { render, unmountComponentAtNode } from "react-dom";
import { act } from "react-dom/test-utils";

import Contact from "./contact";
import MockedMap from "./map";

jest.mock("./map", () => {
  return function DummyMap(props) {
    return (
      <div data-testid="map">
        {props.center.lat}:{props.center.long}
      </div>
    );
  };
});

6. 이벤트
DOM 요소에 실제 DOM 이벤트를 전달하여 결과를 검증

// toggle.test.js

import React from "react";
import { render, unmountComponentAtNode } from "react-dom";
import { act } from "react-dom/test-utils";

import Toggle from "./toggle";

let container = null;
beforeEach(() => {
  // 렌더링 대상으로 DOM 엘리먼트를 설정합니다.
  document.body.appendChild(container);
});

it("changes value when clicked", () => {
  const onChange = jest.fn();
  act(() => {
    render(<Toggle onChange={onChange} />, container);
  });

  // 버튼 엘리먼트를 가져와서 클릭 이벤트를 트리거 하세요.

  const button = document.querySelector("[data-testid=toggle]");
  expect(button.innerHTML).toBe("Turn on");

  act(() => {
    button.dispatchEvent(new MouseEvent("click", { bubbles: true }));
  });

  expect(onChange).toHaveBeenCalledTimes(1);
  expect(button.innerHTML).toBe("Turn off");

  act(() => {
    for (let i = 0; i < 5; i++) {
      button.dispatchEvent(new MouseEvent("click", { bubbles: true }));
    }
  });

  expect(onChange).toHaveBeenCalledTimes(6);
  expect(button.innerHTML).toBe("Turn on");
});

7. 타이머
jest timer Mocks 사용
https://jestjs.io/docs/timer-mocks
8. 스냅샷 테스트
렌더링 된 컴포넌트 출력을 저장하고 제대로 변경사항이 적용되는지 테스트

// hello.test.js, again

import React from "react";
import { render, unmountComponentAtNode } from "react-dom";
import { act } from "react-dom/test-utils";
import pretty from "pretty";

import Hello from "./hello";

it("should render a greeting", () => {
  act(() => {
    render(<Hello />, container);
  });

  expect(
    pretty(container.innerHTML)
  ).toMatchInlineSnapshot(); /* ... gets filled automatically by jest ... */

Enzyme API

  • shallow
    • 간단한 컴포넌트를 메모리 상에 렌더링합니다. 단일 컴포넌트를 테스트할 때 유용합
  • mount
    • HOC나 자식 컴포넌트까지 전부 렌더링합니다. 다른 컴포넌트와의 관계를 테스트할 때 유용
  • render
    • 컴포넌트를 정적인 html로 렌더링. 컴포넌트가 브라우저에 붙었을 때 html로 어떻게 되는지 판단할 때 사용
  • wrapper
    • state()
      • 내부 state 접근
    • props()
    • find()
    • simulate()

E2E 테스트

  • FE 개발의 경우 UI/UX 관련 기능을 실제 사용자 환경과 분리된 상태에서 테스트하기에는 한게가 있기 때문에, 사용자의 관점에서 테스트를 할 수 있는 필요가 있음

  • Cypress

    • 유저 입장에서 렌더링 후에 발생하는 에러를 찾기 좋다
    • 크롬, 일렉트론 브라우저만 지원
    • 브라우저 내부에서 실행됨
    • js만 사용 가능
    • 멀티 탭을 지원하지 않는다
    • 프론트엔드 개발단계에서 사용하기에 좀 더 최적화
  • Playwright

    • puppeteer를 만든팀이 MS로 옮겨가면서 만들게 된 라이브러리, puppeteer와 유사
    • 크롬, 파이어폭스, webkit 사용가능
    • javascript, typescript, python, c#, go 언어 사용 개발 가능
  • Puppeteer

    • 크롬 혹은 크로미움을 Headless 브라우저(GUI가 없는 브라우저) 상태로 조작 할 수 있는 API
  • Testcafe

  • Webdriverio

    • 코드가 쉬움
    • 속도가 느림
    • 기본적인 js 기능 사용할때 적합
    • 지원하는 브라우저가 다양하고 실제로 브라우저를 실행

기타

  • StoryBook
    • 스토리북은 테스트 도구이기 보단 컴포넌틑 단위로 프로젝트를 쪼개고, 문서화를 할 수 있도록 지원하는 디자인 도구
    • UI 개발 환경에 가깝다


https://ko.reactjs.org/docs/testing.html
https://jestjs.io/
https://tecoble.techcourse.co.kr/post/2021-10-22-react-testing-library/
https://velog.io/@binimini/Mocha-vs-Jest-JS-Test-Framework
https://dev.to/bonnie/testing-react-a-converts-journey-from-enzyme-to-testing-library-5ai0
https://youtu.be/3LMmPXoGI9Q
https://youtu.be/pkYUcKWOqPs
https://blog.mathpresso.com/모던-프론트엔드-테스트-전략-1편-841e87a613b2
https://ui.toast.com/fe-guide/ko_TEST
https://npmcompare.com/compare/cypress,playwright,puppeteer,testcafe,webdriverio
https://storybook.js.org/tutorials/ui-testing-handbook/react/ko/introduction/

profile
web front

0개의 댓글