1. 단위테스트와 통합테스트, E2E 개념

  1. 단위테스트(unit test) : 개별 컴포넌트나 함수를 테스트하는 것으로, 각 부분이 제대로 작동하는지 확인할 수 있다. 이 테스트는 설계와 구현 중에 버그를 찾는 데 가장 효과적이다.

  2. 통합테스트(integration test) : 여러 컴포넌트나 시스템을 함께 테스트하여 해당 부분들이 잘 협업하는지 확인한다. 다시말해서 시스템 전체가 잘 작동하는지 판단하기 위한 것이다.

  3. E2E(end-to-end test) : 사용자 관점에서 애플리케이션을 테스트한다. 이것은 실제 사용자 경험을 가장 잘 시뮬레이션하며, 모든 계층 위에서 실행된다.


2. 단위테스트 및 통합테스트

(1) 단위테스트

가장 작은 단위를 테스트 한다. 일반적으로 개별 컴포넌트나 함수의 동작을 검증한다.

  • 단위 테스트는 함수나 메서드와 같은 작은 단위의 코드를 테스트하는 데 중점을 두는 테스트 유형이다. 즉, 시스템의 전체적인 동작에 중점을 맞추지는 않는다.
  • 리액트에서 주로 Jest를 사용하여 단위테스트를 작성한다.
// MyComponent.js
import React from 'react';

const MyComponent = ({ text }) => {
  return <div>{text}</div>;
};

export default MyComponent;
// MyComponent.test.js
import React from 'react';
import { render, screen } from '@testing-library/react';
import MyComponent from './MyComponent';

test('renders text prop', () => {
  render(<MyComponent text="Hello, World!" />);
  const element = screen.getByText(/Hello, World!/i);
  expect(element).toBeInTheDocument();
});

(2) 통합테스트

통합 테스트는 여러 컴포넌트나 모듈이 함께 동작하는 방식을 테스트 한다.

  • 통합 테스트는 시스템의 여러 단위 또는 구성 요소 간의 상호 작용을 검증하는 테스트 유형이다.
  • 서로 다른 단위가 함께 동작하면서 흐름에 맞게 잘 동작하고, 예상한 결과를 생성하는지를 테스트한다.
  • 특징으로는 여러 컴포넌트나 모듈의 상호작용을 검증하며, 단위테스트보다 복잡하다.
  • 리액트에선 Jest, React-testing-library 를 많이 사용한다.
// App.js
import React from 'react';
import Header from './Header';
import Footer from './Footer';

const App = () => {
  return (
    <div>
      <Header />
      <main>Content goes here</main>
      <Footer />
    </div>
  );
};
// App.test.js
import React from 'react';
import { render, screen } from '@testing-library/react';
import App from './App';

test('renders Header and Footer', () => {
  render(<App />);
  expect(screen.getByText(/Header/i)).toBeInTheDocument();
  expect(screen.getByText(/Footer/i)).toBeInTheDocument();
});

export default App;

(3) E2E 테스트(End-to-End Test)

E2E 테스트는 애플리케이션의 전체 흐름을 테스트 한다. 사용자 관점에서 애플리케이션이 예상대로 동작하는지 검증한다.

  • 시스템의 시작부터 끝까지 전체 흐름을 확인하는 테스트 유형
  • 시스템이 예상대로 작동하고, 사용자의 요구 사항을 충족하는지 확인하기위해 상호작용을 테스트하는 것이 포함됨.
  • 예시로 상품 구매, 로그인, 여러 기능들이 해당된다.
  • 리액트에서는 주로 PlaywrightCypress를 사용하여 E2E 테스트를 작성한다.

Playwright

// e2e.spec.js
const { test, expect } = require('@playwright/test');

test('homepage has title', async ({ page }) => {
  await page.goto('http://localhost:3000');
  await expect(page).toHaveTitle(/React App/i);
});

Cypress

// e2e.cy.js
describe('Homepage', () => {
  it('should have the correct title', () => {
    cy.visit('http://localhost:3000');
    cy.title().should('include', 'React App');
  });
});

3. 각각의 테스트에 대한 장단점

  • 단위 테스트는 개별 기능들이 모두 제대로 작동하는지 검증할 수 있어서 장애를 빨리 발견하고 수정할 수 있는 장점이 있다. 하지만, 모든 컴포넌트를 테스트해야한다는 점에서는 상당한 작업량과 시간이 소요된다.

  • 통합 테스트는 개별적인 기능들이 아니라 서로 연결된 기능들이 원활하게 동작하는지 검증하기 때문에, 전체 시스템의 안정성을 평가할 수 있다는 장점이 있다. 그러나, 단위 테스트보다 문제를 파악하고 해결하는데 더 많은 시간이 소요될 수 있다.

  • E2E 테스트는 실질적인 사용자 경험을 가장 잘 반영하지만, 설정 자체가 복잡하고 실행 시간도 오래 걸리며, 실패한 경우 어디서 문제가 발생했는지 찾기 어렵다는 단점이 있다.


4. E2ETest Best Practices

사내에서 주로 E2E 테스트를 많이 썼기 때문에 관련하여 어떻게 작성하면 좋을지 생각해 봤다.(Playwright)

Testing philosophy

  • 사용자에게 보이는 영역/행위 테스트

최종 사용자가 페이지를 보거나, 상호작용이 가능한 페이지에 대해서 테스트를 수행한다.

테스트 작성 시 가능하면 고립

테스트 각각에 대해서 격리해서 작업(local/session storage, data, cookies등)
테스트에 특정 반복을 피해야할 경우 before and after hooks를 이용
테스트 파일 내에서 특정 URL 이동, 앱의 이부에 로그인 진행 시 before hooks 이용

import { test } from '@playwright/test';

test.beforeEach(async ({ page }) => {
  // Runs before each test and signs in each page.
  await page.goto('https://github.com/login');
  await page.getByLabel('Username or email address').fill('username');
  await page.getByLabel('Password').fill('password');
  await page.getByRole('button', { name: 'Sign in' }).click();
});

test('first', async ({ page }) => {
  // page is signed in.
});

test('second', async ({ page }) => {
  // page is signed in.
});

전체적인 흐름이 맞는 테스트 코드 작성(긴 테스트도 괜찮다.)

전체 앱 흐름을 테스트 할 경우 여러 작업에 대한 assertion 처리로 괜찮다.
긴 테스트를 굳이 개별 테스트로 나눌 경우 테스트 실행 속도를 늦추기 때문에 피해야 한다.
긴 테스트 에러 발생 시 종료하지 않고 에러만 표시할 경우 soft assertion 사용 가능하다.

// Make a few checks that will not stop the test when failed...
await expect.soft(page.getByTestId('status')).toHaveText('Success');

// ... and continue the test to check more things.
await page.getByRole('link', { name: 'next page' }).click();

third-party 종속된 테스트는 피하라

await page.route('**/api/fetch_data_third_party_dependency', route =>
  route.fulfill({
    status: 200,
    body: testData,
  }),
);
await page.goto('https://example.com');

Use chaining and filtering

페이지의 특정 부분을 검색할 경우 chaning, filtering으로 좁히는 걸 추천

const product = page.getByRole('listitem').filter({ hasText: 'Product 2' });

await page
  .getByRole('listitem')
  .filter({ hasText: 'Product 2' })
  .getByRole('button', { name: 'Add to cart' })
  .click();

playwright에 내장된 locator를 사용해라

auto waiting, retry-ability 등의 기능을 사용하기 위해서 내장된 locator를 이용

page.getByRole('button', { name: 'submit' })

5. 결론

프로젝트의 크기와 중요성, 그리고 가능한 리소스 등을 고려하여 결정하는 것이 좋다. 만일 코드베이스가 작아 변경사항에 따른 영향력을 쉽게 파악할 수있다면, 단위 테스트를 진행하는 것이 좋을 수 있다. 반면에 프로젝트가 크고 복잡한 상호작용이 많다면, 통합 테스트나 E2E 테스트를 고려해보는 것도 좋다.

6. 참고자료

Playwright 테스트

profile
끊임없이 발전해가는 개발자.

0개의 댓글