주니어로서 마주하는 TDD (上)

개발 블로그·2022년 4월 8일
0
post-thumbnail

서론

서비스 회사들의 채용공고를 보다보면 " 테스팅 코드 작성을 해보신 분 " 또는 " Test 기반 개발 경험(TDD) "이란 항목들을 마주친 적이 있을 것입니다. 저 또한 대체 테스팅 코드는 프론트단에서는 어떻게 짜는 것이지?라는 의문으로 이 글을 작성하게 되었습니다.

테스트는 무엇인가?

프로그램을 실행하여 오류와 결함을 검출하고 애플리케이션이 요구사항에 맞게 작동하는 지 검증하는 절차를 의미합니다. 테스트를 함으로써 발생 가능한 결함을 예방하고 개발 과정에서 생긴 변경사항들로 인해 새로운 결함이 생기지 않았는지 확인할 수 있습니다.

테스팅 코드를 작성하면서 얻는 장점들

  1. 나의 코드에 신뢰가 생긴다.

코드가 작동하면 어떤 결과가 나오는지 명확히 명시하게 되면서 부수적으로 코드에 대한 자신감이 생기게 됩니다.
의식적으로 테스팅코드를 작성하다보니 스펙을 더 명확하게 하게 되고 요구사항에 좀 더 집중할 수 있는 개발이 될 수 있습니다. 예외사항을 재현하는 것 또한 태스팅 코드가 유리합니다.

  1. 클린 코드

예를 들어 투두리스트를 구현한다고 하면 명확히 작동해야 하는 동작은 할일 추가, 수정, 삭제, 생성이 있을 것입니다. 테스팅 코드를 통해서 데이터가 올바르게 변경되는지 작성해놓으면 로직을 리팩토링할 때 쾌적함을 느끼게 될 것입니다.

할일을 추가하는 로직이 비효율적으로 작성되어있다 생각하면 다른 것과의 인과관계를 생각할 필요없이 할일을 효율적으로 데이터에 추가하는 것만 집중할 수 있습니다.

  1. 그 자체로 문서화된다.

어플리케이션이 복잡해질수록 우리는 해당 UI의 구체적인 스펙을 기억할 수 없고 코드를 확인해야 되었지만 이제는 테스팅에 명시된 케이스들로 확인가능합니다.
어떤 페이지에서 신규 기능을 추가하게 되었을 때 기존의 기능들 중 동작이 안되는 것이 있는지 뷰에 표현되지 않는 것이 있는지 우리는 어떤 행동없이도 테스팅 툴로 확인가능하게 됩니다.

테스팅 종류

  1. Static Test (정적 테스트)

    • 구문 오류, 나쁜 스타일의 코드 등을 검증
    • ESLint, Typescript
  2. Unit Test

    • 작은 단위를 떼어내어 분리된 환경에서 테스트
    • 복잡한 알고리즘이 제대로 동작하는지 확인
    • Mocking이 필요
    • Jest
  3. Intergration Test

    • 어플리케이션의 여러 부분들이 통합되어 제대로 상호작용되는지 테스트
    • 주로 큰 범위의 테스트를 의미
    • Jest, Enzyme
  4. End To End Test (E2E)

    • 실제 사용자가 사용하는 것과 같은 조건환경에서 전체 시스템 테스트
    • API 서버, DB 등 외부 서비스를 모두 사용하여 통합 테스트
    • Cypress
    • 보통 이런 툴들은 외부 라이브러리와 호환을 지원해준다.

프론트엔드의 테스팅

프론트엔드에서 테스트할 것은 크게 세가지로 볼 수 있다.

  • 시각적 요소 vs 기능적 요소
  • 사용자의 이벤트 처리
  • API 서버 통신

시각적 테스트

시각적 테스트는 기본적으로 DOM API가 지원되는 브라우저 환경에서 진행되어야 합니다.

  • 생성된 HTML, CSS 코드가 브라우저에서 제대로 렌더링되는가?
  • 레이아웃, 폰트, 데이터 변경으로 인한 차이가 있는가?
// Login.test.js
import React from "react";
import { render, screen } from "@testing-library/react";
import Login from "./Login";

test("username input should be rendered", () => {
		render(<Login />);

		const userInputEl = screen.getByPlaceholderText(/username/i);
		expect(userInputEl).toBeInTheDocument();
});

해당 코드는 DOM에 인풋 태그가 그려지는지 체크하는 코드입니다.
최근에는 스냅샷 테스트라는 것을 지원해서 기존 문서와 일치하면 통과시키는 방법을 사용하고 있습니다.

// HookCounter.test.js
import React from "react";
import { mount } from "enzyme";
import HookCounter from "./HookCounter";

describe("<HookCounter />", () => {
	it("matches snapshot", () => {
		const wrapper = mount(<HookCounter />);
		expect(wrapper).toMatchSnapshot();
	});
});
// __snapshots__/HookCounter.test.js.snap

exports[`<HookCounter /> matches snapshot 1`] = `
<HookCounter>
  <div>
    <h2>
      0
    </h2>
    <button
      onClick={[Function]}
    >
      +1
    </button>
    <button
      onClick={[Function]}
    >
      -1
    </button>
  </div>
</HookCounter>
`;

스냅샷으로 생성된 파일과 현재 DOM을 비교해서 차이가 생기면 에러를 뱉는 식으로 진행됩니다. 의도된 변화면 스냅샷을 갱신할 수도 있습니다.

그러나 이 방법에도 한계는 존재합니다. 결국 코드로 문제가 없다고 하더라도 눈으로 보지 않는한 의도한 결과물이 적용되었는지 알 수 없습니다. 그리고 리팩토링에도 큰 도움이 되지 않습니다. 극단적인 예로 외부 css 하나가 import되지 않아 모두 깨져버릴 수도 있습니다.

시각적 요소는 결국 픽셀 정보가 중요할 수밖에 없습니다. 실제로 그림이 바뀌었는지 체크하는 것이 중요하고 이런 것들을 테스팅하는 방법을 시각적 회귀 테스트라고 부릅니다.

시각적 회귀 테스트

아직까지는 디자인 시안을 던져서 그것이랑 일치하는지까지 확인은 되지 않지만 우리가 눈으로 확인한 후 업데이트했을 때 브라우저를 캡쳐해 이미지 비교를 하는 툴들이 많이 나왔습니다.

1. 시각적 회귀 테스트의 문제점

  • 캡처 이미지의 신뢰성 :
    운영체제, 브라우저 등 렌더링 방식, 캡처 시점까지 영향 받을 수 있음(마우스 커서가 올라가잇는 것까지 영향받을 수 있다고 함)

  • 결과 확인 및 이력 관리 :
    브라우저, 뷰포트, 디바이스 별로 이미지 파일을 생성하고 관리해야 하며 결과 확인을 위한 UI가 추가적으로 필요함

2. 시각적 테스트 전문 도구

3. Storybook


독립된 UI 컴포넌트 개발환경 서버를 제공하는 라이브러리입니다. 테스팅 코드로 작성할 때는 각 상태를 다 작성해야 하지만 스토리북은 다양한 상태변화로 바뀌는 UI의 모습을 등록하고 체계적으로 관리할 수 있습니다.

시각적 테스트 전문 도구에서도 많이 지원합니다.


기능적 테스트

기능에 있어서는 브라우저가 없어도 Node.js 환경에서도 테스트 가능합니다.

1. 사용자 이벤트 처리

import React from "react";
import { fireEvent, render, screen, waitFor } from "@testing-library/react";
import Login from "./Login";

test("button should not be disabled when inputs exist", () => {
		render(<Login />);
		const userButtonEl = screen.getByRole("button");
		const userInputEl = screen.getByPlaceholderText(/username/i);
		const userPasswordEl = screen.getByPlaceholderText(/password/i);
		const testValue = "test";

		fireEvent.change(userInputEl, { target: { value: testValue } });
		fireEvent.change(userPasswordEl, { target: { value: testValue } });
		expect(userButtonEl).not.toBeDisabled();
});

testing library에서 지원하는 메소드로 브라우저 이벤트를 테스트하고 정상적으로 작동하는지 확인할 수 있습니다.

2. API 서버 통신

import React from "react";
import { fireEvent, render, screen, waitFor } from "@testing-library/react";
import Login from "./Login";

jest.mock("axios", () => ({
	__esModule: true,

	default: {
		get: () => ({
			data: { id: 1, name: "john" },
		}),
	},
}));

test("user should be rerendered after fetching", async () => {
		render(<Login />);
		const userButtonEl = screen.getByRole("button");
		const userInputEl = screen.getByPlaceholderText(/username/i);
		const userPasswordEl = screen.getByPlaceholderText(/password/i);
		const testValue = "test";

		fireEvent.change(userInputEl, { target: { value: testValue } });
		fireEvent.change(userPasswordEl, { target: { value: testValue } });
		fireEvent.click(userButtonEl);

		const userItem = await screen.findByText("john");
		expect(userItem).toBeInTheDocument();
});

Jest에서 지원하는 Mocking을 통하여 비동기 함수 처리도 테스팅 가능합니다.

3. Cypress

Jest로는 어느 단계에서 깨졌는지 디버깅이 힘든 단점이 있어서 Cypress를 통해 해결할 수 있습니다.

  • 브라우저의 디버깅 툴과 리덕스, 리액트 익스텐션을 활용한 디버깅
  • 손쉬운 서버 요청/응답 목킹
  • 실행된 모든 명령의 데이터들을 확인 가능합니다.

결론

테스팅은 시간과 노력, 기술력이 필요하지만 그만한 가치도 있다는 게 제 생각입니다.
다음에는 본격적으로 TDD에 관한 이야기를 하고자 합니다.

  1. 내가 작성한 테스트가 신뢰를 주는지 항상 의심하자.
  2. 테스트는 비용이다. 불필요한 테스트를 최소화하자.
  3. 시각적 테스트와 기능적 테스트를 분리하자.
  4. 시각적 테스트를 자동화할 때는 전문 테스트 도구 사용을 고려하자.
  5. 모듈 단위 시스템보다 컴포넌트 단위의 통합 테스트를 먼저 생각하자.
  6. StoryBook을 사용하면 시각적 요소를 편리하게 개발/테스트할 수있다.
  7. Cypress를 사용하면 기능적 요소를 편리하게 개발/테스트할 수있다.

https://www.youtube.com/watch?v=q9d631Nl0_4

profile
프론트엔드 개발자의 TIL

0개의 댓글