1. 작성 동기, 테스트 코드란?
1-1. 영감을 얻은 동료의 멘트

스토리포인트를 산정할 때 테스트시간을 충분히 고려하지 못했다는 동료의 멘트를 봤습니다.
(스토리 포인트란 업무 산정 단위로, 저희 회사에서는 1시간을 기준으로 합니다.)
기획팀장님이 해당 동료에게 책을 하나 추천해주셨는데요. 이런 내용이 담겨 있었습니다.

저 말을 한 사람은 프데더릭 브룩스라고.. 대단하신 분입니다. (노벨상 수상, IBM에서 개발 지휘)
자신의 유명한 저서인 맨먼스 미신이라는 책에 저런 구절이 있다고 합니다.
저 역시 비슷한 고민을 하고 있었습니다. 현업에서 반복되는 수동적 테스트를 개선할 방법이 고민이었습니다. 나름의 방법을 연구해보기 위해서 이 글을 작성해봅니다.
1-2. 먼저 테스트 코드란?

좌측처럼 Form을 만든다고 했을 때, 사람이 테스트를 한다고 해봅니다.
그럼 우측처럼 테스트케이스를 나열할 수 있을 것입니다.
진행한 총 케이스는 이렇습니다.
- 닫기 버튼을 눌렀을 때 닫히는지?
- 등록버튼을 눌렀을 때 입력 값이 제대로 된 케이스일까?
- 등록버튼을 눌렀을 때 입력 값이 이상하다면?
이렇게 하나하나 테스트 하는 것인데, 이 것이 자동화된다고 생각하시면 됩니다.
2. 테스트 코드를 작성해두는 것의 장점
2-1. 테스트가 자동화된다!
기능을 추가하거나 수정할 때, 했던 테스트를 반복해서 진행하지 않아도 됩니다. 테스트 코드가 알아서 해주니까요!
테스트코드는 리팩토링할 때 유용한 것 같습니다. 테스트코드를 짜놓고 리펙토링을 하게되면 리펙토링 하기 전 과 후의 결과가 동일하다는 것을 보장할 수 있으니까요.
리펙토링 뿐 아니라 새로운 기능이 추가되거나 기존 기능이 변경되었을 때 다른 기능들이 제대로 동작한다는 것을 보장할 수 있기에 유지보수 측면에서도 좋다고 생각합니다.
2-2. 케이스를 팀원들도 볼 수 있음
테스트 케이스를 작성해둔 것 만으로 팀원이 어떤 테스트를 이미 진행했는지 알 수 있어서 효율적입니다. 이미 테스트 코드를 짜둔 상태라면 내가 굳이 테스트를 할 필요가 없겠지요. 팀원이 테스트를 해야한다는 부담감과 시간을 덜어줄 수 있는 것입니다.
3. 한계점도 있다!
3-1. 테스트 코드를 일일히 작성해야함
테스트 케이스를 누군가가 한땀한땀 작성해두어야 합니다. 초기비용이 드는 것이지요..
다만! 요즘 AI가 매우 발전하여, 해당 케이스도 자동 작성하는 방법이 있을 수도 있습니다.
시험삼아 Chat GPT에게 물어봤을 때에는 아주 잘 해주더군요.

3-2. 코드가 급격하게 변하지 않아야 한다
제작한 프로덕트의 생김새가 크게 변하지 않아야 합니다. 프로덕트 생김새가 크게 변한다면 테스트 코드를 전체 다시 짜야하는 불상사가 발생할 수도 있으니까요.
4. 테스트 코드 작성 예시
4-1. 텍스트 예시
// Profile.js
import React from "react";
const Profile = ({ username, name }) => {
return (
<div>
<p>{username}</p>
<span>({name})</span>
</div>
);
};
export default Profile;
// Profile.test.js
import React from "react";
import { render, screen } from "@testing-library/react";
import Profile from "./Profile";
describe("<Profile />", () => {
it("shows the props correctly", () => {
render(<Profile username="kyungsle" name="이경수" />);
const linkElement = screen.getByText(/kyungsle/i);
expect(linkElement).toBeInTheDocument();
});
});
해당 테스트는 간단하게, kyungsle이라는 텍스트가 해당 컴포넌트 안에 존재하는 지에 대한 것입니다.
개인적으로 사용하면서 느낀 사용법은 이렇습니다.
- render이라는 메서드가 테스트환경에서 그려주는 역할
- screen라는 객체가 화면 정보를 가져오는 역할
- linkElement 가 document안에 있는지 검증
이 정도로만 테스트 코드를 작성하지는 않겠지요. 이제 작성한 이벤트를 테스트하는 법을 알아봅니다.
4-2. 폼 예시
// Dialog.js
import React, { useState } from "react";
export default function Dialog({ title, description, onClickClose, onClickSubmit }) {
const [text, setText] = useState("");
function handleClickSubmit() {
const stringOrNumberRegex = /^[a-zA-Z0-9]+$/;
switch (true) {
case text === "":
console.error("입력값이 없는데요..");
break;
case stringOrNumberRegex.test(text) === false:
alert("똑바로 좀 입력해");
break;
default:
onClickSubmit(text);
console.log("입력 보내기 성공");
}
}
return (
<div>
<h2>{title}</h2>
<div>{description}</div>
<div>
<input
type="text"
placeholder="contents"
name="contents"
onChange={(e) => setText(e.target.value)}
value={text}
/>
</div>
<div>
<button type="button" onClick={onClickClose}>
닫기
</button>
<button type="button" onClick={handleClickSubmit}>
등록
</button>
</div>
</div>
);
}
// Dialog.test.js
import React from 'react';
import { render, screen, fireEvent } from '@testing-library/react';
import context from 'jest-plugin-context';
import Dialog from './Dialog';
describe('Dialog', () => {
// window alert를 테스트하기 위해 필요
jest.spyOn(window, 'alert').mockImplementation(() => {});
const title = '제목제목~';
const description = '설명설명~';
const handleClickClose = jest.fn(); // fn : mock up function
const handleClickSubmit = jest.fn();
const spyOnConsoleError = jest.spyOn(console, 'error');
const renderDialog = () =>
render(
<Dialog
title={title}
description={description}
onClickClose={handleClickClose}
onClickSubmit={handleClickSubmit}
/>
);
it('title과 description이 보여야 한다.', () => {
const { container } = renderDialog();
expect(container).toHaveTextContent(title);
expect(container).toHaveTextContent(description);
});
context('닫기 버튼을 누르면', () => {
it('handleClickClose가 호출되어야 한다.', () => {
renderDialog();
fireEvent.click(screen.getByText('닫기'));
expect(handleClickClose).toBeCalled();
});
});
context('등록버튼을 눌렀을 때', () => {
context('입력된 텍스트가 없다면', () => {
it('console.error에 "입력값이 없는데요.." 문구가 찍혀야 한다.', () => {
renderDialog();
fireEvent.click(screen.getByText('등록'));
expect(spyOnConsoleError).toBeCalledWith('입력값이 없는데요..');
});
});
context('입력된 텍스트가 있다면', () => {
context('제대로 된 입력값이라면(영어/숫자/공백)', () => {
const sampleText = 'sookhyehyungseungbeensoo6junior';
it('handleClickSubmit에 입력값이 전달되어야 한다.', () => {
renderDialog();
fireEvent.change(screen.getByPlaceholderText('contents'), {
target: { name: 'contents', value: sampleText },
});
fireEvent.click(screen.getByText('등록'));
expect(handleClickSubmit).toBeCalledWith(sampleText);
});
});
context('이상한 입력값이 있다면', () => {
const sampleText = '안녕안녕나는야~!~!';
it('console.error에 "똑바로 좀 입력해" 문구가 찍혀야 한다.', () => {
renderDialog();
fireEvent.change(screen.getByPlaceholderText('contents'), {
target: { name: 'contents', value: sampleText },
});
fireEvent.click(screen.getByText('등록'));
expect(window.alert).toBeCalledWith('똑바로 좀 입력해');
});
});
});
});
});
이 코드에서 검증한 테스트는 이렇습니다.
- 화면이 켜졌을 시 : 제목과 설명이 보이는가?
- 닫기 버튼을 누르면 닫는 함수가 실행되는가?
- 등록 버튼을 눌렀을 때 입력 텍스트가 없으면 : 입력값이 없다는 console를 출력하는가?
- 등록 버튼을 눌렀을 때 입력 텍스트가 있다면
- 제대로 된 값이라면 submit함수가 실행되는가?
- 이상한 값이라면 console error를 출력하는가?
jest 자체에서 구현한 함수에 대해서 설명하자면 이렇습니다.
- jest.spyOn 함수는 주어진 객체의 메서드를 스파이 함수로 대체하는 역할
- mockImplementation 함수는 모의 함수의 구현을 제공하는 역할
- fireEvent 함수는 테스트 시에 DOM 이벤트를 발생시키는 역할
더 자세한 함수 사용 방법들은 아래 링크를 참고하는 것이 좋습니다~!
https://jestjs.io/docs/jest-object
https://testing-library.com/docs/
제 레포에서의 테스트 코드를 직접 실행해보셔도 좋습니다.
https://github.com/keinn51/prac_react_testing_library
여기서 사용한 Describe - Context - it 테스트 패턴은 좀 재밌어 보입니다.
더 살펴보도록 합니다.
5. Outro
5-1. Describe - Context - it
Describe - Context - it 은 BDD 패턴 중 하나입니다.
BDD는 Behavior Driven Development로 테스트를 시나리오 기반으로 작성하는 것을 말합니다.
BDD에는 두 가지 패턴이 있고, 더 있을 수 있는데 그 것까지는 모르겠습니다..ㅎㅎ
이 글에서는 Describe - Context - it 의 느낌만 알아가면 됩니다.
- Given-When-Then : [주어진 조건 - 테스트 행동 - 검증 로직] 을 순서로 코드를 작성하는 것을 말합니다.
- Describe - Context - it : [테스트 대상 - 테스트 대상이 놓인 상황 - 테스트 대상이 해당 상황에서 해야하는 행동] 을 순서로 코드를 작성하는 것입니다. 마치.. 시나리오를 작성하는 것처럼??
이 것만 들었을 때에는 아무리 이해심이 높은 사람이라도 이해하기 어려울 것입니다.
테스트 대상을 위한 시나리오를 작성하는 거라고 생각하면 이해가 편하지 않을까 합니다.
위에서 사용했던 상황을 가져와서 살펴봅니다.

- 테스트 대상 → Dialog
- 테스트 대상이 놓인 상황 → 닫기 버튼을 누르면 / 입력된 텍스트가 없다면 등등
- 테스트 대상이 해당 상황에서 해야하는 행동 → title과 description 이 보여야 한다 등등
describe("Dialog", () => {
context("닫기 버튼을 누르면", () => {
it("handleClickClose가 호출되어야 한다.", () => {
renderDialog();
fireEvent.click(screen.getByText("닫기"));
expect(handleClickClose).toBeCalled();
});
});
context("등록버튼을 눌렀을 때", () => {
context("입력된 텍스트가 없다면", () => {
it('console.error에 "입력값이 없는데요.." 문구가 찍혀야 한다.', () => {
renderDialog();
fireEvent.click(screen.getByText("등록"));
expect(spyOnConsoleError).toBeCalledWith("입력값이 없는데요..");
});
});
});
});
코드로 보면 더욱 이해가 갑니다. context가 중첩되는 것도 코드로 따로 빼고 싶은 생각이 드는군요 후후😟 더 알고 싶다면 아래의 글을 추천합니다 좋더군요.
https://kooku0.github.io/blog/프론트엔드에서-테스트코드 짜기/#프론트엔드와-궁합이-잘-맞는-bdd
5-2. 더 생각해봐야 하는 것
- 테스트 코드 자체의 작성을 자동으로 해주는 npm 라이브러리가 있지 않을까? 요즘 AI시대인데..
- 테스트 코드 작성 여부를 코드 리뷰에서 검증하기 (리뷰 자동화도 되면 좋겠다!)
'Develop' 카테고리의 다른 글
lazy loading으로 Entry Point File 크기 줄이는 방법 연구 (0) | 2024.07.20 |
---|---|
React context 대신 Event bus를 사용해 렌더링 개선하는 방법 조사 (0) | 2024.07.20 |
drag event 성능 개선을 위한 JS web worker 사전 조사 (0) | 2024.07.20 |
React : performamce, profiler를 통한 성능 개선 측정하기 (drag & drop, state refactoring) (0) | 2024.07.20 |
쿠키로 마음을 전하고 싶으면 도메인부터 맞추자 🍪 (0) | 2024.07.20 |
1. 작성 동기, 테스트 코드란?
1-1. 영감을 얻은 동료의 멘트

스토리포인트를 산정할 때 테스트시간을 충분히 고려하지 못했다는 동료의 멘트를 봤습니다.
(스토리 포인트란 업무 산정 단위로, 저희 회사에서는 1시간을 기준으로 합니다.)
기획팀장님이 해당 동료에게 책을 하나 추천해주셨는데요. 이런 내용이 담겨 있었습니다.

저 말을 한 사람은 프데더릭 브룩스라고.. 대단하신 분입니다. (노벨상 수상, IBM에서 개발 지휘)
자신의 유명한 저서인 맨먼스 미신이라는 책에 저런 구절이 있다고 합니다.
저 역시 비슷한 고민을 하고 있었습니다. 현업에서 반복되는 수동적 테스트를 개선할 방법이 고민이었습니다. 나름의 방법을 연구해보기 위해서 이 글을 작성해봅니다.
1-2. 먼저 테스트 코드란?

좌측처럼 Form을 만든다고 했을 때, 사람이 테스트를 한다고 해봅니다.
그럼 우측처럼 테스트케이스를 나열할 수 있을 것입니다.
진행한 총 케이스는 이렇습니다.
- 닫기 버튼을 눌렀을 때 닫히는지?
- 등록버튼을 눌렀을 때 입력 값이 제대로 된 케이스일까?
- 등록버튼을 눌렀을 때 입력 값이 이상하다면?
이렇게 하나하나 테스트 하는 것인데, 이 것이 자동화된다고 생각하시면 됩니다.
2. 테스트 코드를 작성해두는 것의 장점
2-1. 테스트가 자동화된다!
기능을 추가하거나 수정할 때, 했던 테스트를 반복해서 진행하지 않아도 됩니다. 테스트 코드가 알아서 해주니까요!
테스트코드는 리팩토링할 때 유용한 것 같습니다. 테스트코드를 짜놓고 리펙토링을 하게되면 리펙토링 하기 전 과 후의 결과가 동일하다는 것을 보장할 수 있으니까요.
리펙토링 뿐 아니라 새로운 기능이 추가되거나 기존 기능이 변경되었을 때 다른 기능들이 제대로 동작한다는 것을 보장할 수 있기에 유지보수 측면에서도 좋다고 생각합니다.
2-2. 케이스를 팀원들도 볼 수 있음
테스트 케이스를 작성해둔 것 만으로 팀원이 어떤 테스트를 이미 진행했는지 알 수 있어서 효율적입니다. 이미 테스트 코드를 짜둔 상태라면 내가 굳이 테스트를 할 필요가 없겠지요. 팀원이 테스트를 해야한다는 부담감과 시간을 덜어줄 수 있는 것입니다.
3. 한계점도 있다!
3-1. 테스트 코드를 일일히 작성해야함
테스트 케이스를 누군가가 한땀한땀 작성해두어야 합니다. 초기비용이 드는 것이지요..
다만! 요즘 AI가 매우 발전하여, 해당 케이스도 자동 작성하는 방법이 있을 수도 있습니다.
시험삼아 Chat GPT에게 물어봤을 때에는 아주 잘 해주더군요.

3-2. 코드가 급격하게 변하지 않아야 한다
제작한 프로덕트의 생김새가 크게 변하지 않아야 합니다. 프로덕트 생김새가 크게 변한다면 테스트 코드를 전체 다시 짜야하는 불상사가 발생할 수도 있으니까요.
4. 테스트 코드 작성 예시
4-1. 텍스트 예시
// Profile.js
import React from "react";
const Profile = ({ username, name }) => {
return (
<div>
<p>{username}</p>
<span>({name})</span>
</div>
);
};
export default Profile;
// Profile.test.js
import React from "react";
import { render, screen } from "@testing-library/react";
import Profile from "./Profile";
describe("<Profile />", () => {
it("shows the props correctly", () => {
render(<Profile username="kyungsle" name="이경수" />);
const linkElement = screen.getByText(/kyungsle/i);
expect(linkElement).toBeInTheDocument();
});
});
해당 테스트는 간단하게, kyungsle이라는 텍스트가 해당 컴포넌트 안에 존재하는 지에 대한 것입니다.
개인적으로 사용하면서 느낀 사용법은 이렇습니다.
- render이라는 메서드가 테스트환경에서 그려주는 역할
- screen라는 객체가 화면 정보를 가져오는 역할
- linkElement 가 document안에 있는지 검증
이 정도로만 테스트 코드를 작성하지는 않겠지요. 이제 작성한 이벤트를 테스트하는 법을 알아봅니다.
4-2. 폼 예시
// Dialog.js
import React, { useState } from "react";
export default function Dialog({ title, description, onClickClose, onClickSubmit }) {
const [text, setText] = useState("");
function handleClickSubmit() {
const stringOrNumberRegex = /^[a-zA-Z0-9]+$/;
switch (true) {
case text === "":
console.error("입력값이 없는데요..");
break;
case stringOrNumberRegex.test(text) === false:
alert("똑바로 좀 입력해");
break;
default:
onClickSubmit(text);
console.log("입력 보내기 성공");
}
}
return (
<div>
<h2>{title}</h2>
<div>{description}</div>
<div>
<input
type="text"
placeholder="contents"
name="contents"
onChange={(e) => setText(e.target.value)}
value={text}
/>
</div>
<div>
<button type="button" onClick={onClickClose}>
닫기
</button>
<button type="button" onClick={handleClickSubmit}>
등록
</button>
</div>
</div>
);
}
// Dialog.test.js
import React from 'react';
import { render, screen, fireEvent } from '@testing-library/react';
import context from 'jest-plugin-context';
import Dialog from './Dialog';
describe('Dialog', () => {
// window alert를 테스트하기 위해 필요
jest.spyOn(window, 'alert').mockImplementation(() => {});
const title = '제목제목~';
const description = '설명설명~';
const handleClickClose = jest.fn(); // fn : mock up function
const handleClickSubmit = jest.fn();
const spyOnConsoleError = jest.spyOn(console, 'error');
const renderDialog = () =>
render(
<Dialog
title={title}
description={description}
onClickClose={handleClickClose}
onClickSubmit={handleClickSubmit}
/>
);
it('title과 description이 보여야 한다.', () => {
const { container } = renderDialog();
expect(container).toHaveTextContent(title);
expect(container).toHaveTextContent(description);
});
context('닫기 버튼을 누르면', () => {
it('handleClickClose가 호출되어야 한다.', () => {
renderDialog();
fireEvent.click(screen.getByText('닫기'));
expect(handleClickClose).toBeCalled();
});
});
context('등록버튼을 눌렀을 때', () => {
context('입력된 텍스트가 없다면', () => {
it('console.error에 "입력값이 없는데요.." 문구가 찍혀야 한다.', () => {
renderDialog();
fireEvent.click(screen.getByText('등록'));
expect(spyOnConsoleError).toBeCalledWith('입력값이 없는데요..');
});
});
context('입력된 텍스트가 있다면', () => {
context('제대로 된 입력값이라면(영어/숫자/공백)', () => {
const sampleText = 'sookhyehyungseungbeensoo6junior';
it('handleClickSubmit에 입력값이 전달되어야 한다.', () => {
renderDialog();
fireEvent.change(screen.getByPlaceholderText('contents'), {
target: { name: 'contents', value: sampleText },
});
fireEvent.click(screen.getByText('등록'));
expect(handleClickSubmit).toBeCalledWith(sampleText);
});
});
context('이상한 입력값이 있다면', () => {
const sampleText = '안녕안녕나는야~!~!';
it('console.error에 "똑바로 좀 입력해" 문구가 찍혀야 한다.', () => {
renderDialog();
fireEvent.change(screen.getByPlaceholderText('contents'), {
target: { name: 'contents', value: sampleText },
});
fireEvent.click(screen.getByText('등록'));
expect(window.alert).toBeCalledWith('똑바로 좀 입력해');
});
});
});
});
});
이 코드에서 검증한 테스트는 이렇습니다.
- 화면이 켜졌을 시 : 제목과 설명이 보이는가?
- 닫기 버튼을 누르면 닫는 함수가 실행되는가?
- 등록 버튼을 눌렀을 때 입력 텍스트가 없으면 : 입력값이 없다는 console를 출력하는가?
- 등록 버튼을 눌렀을 때 입력 텍스트가 있다면
- 제대로 된 값이라면 submit함수가 실행되는가?
- 이상한 값이라면 console error를 출력하는가?
jest 자체에서 구현한 함수에 대해서 설명하자면 이렇습니다.
- jest.spyOn 함수는 주어진 객체의 메서드를 스파이 함수로 대체하는 역할
- mockImplementation 함수는 모의 함수의 구현을 제공하는 역할
- fireEvent 함수는 테스트 시에 DOM 이벤트를 발생시키는 역할
더 자세한 함수 사용 방법들은 아래 링크를 참고하는 것이 좋습니다~!
https://jestjs.io/docs/jest-object
https://testing-library.com/docs/
제 레포에서의 테스트 코드를 직접 실행해보셔도 좋습니다.
https://github.com/keinn51/prac_react_testing_library
여기서 사용한 Describe - Context - it 테스트 패턴은 좀 재밌어 보입니다.
더 살펴보도록 합니다.
5. Outro
5-1. Describe - Context - it
Describe - Context - it 은 BDD 패턴 중 하나입니다.
BDD는 Behavior Driven Development로 테스트를 시나리오 기반으로 작성하는 것을 말합니다.
BDD에는 두 가지 패턴이 있고, 더 있을 수 있는데 그 것까지는 모르겠습니다..ㅎㅎ
이 글에서는 Describe - Context - it 의 느낌만 알아가면 됩니다.
- Given-When-Then : [주어진 조건 - 테스트 행동 - 검증 로직] 을 순서로 코드를 작성하는 것을 말합니다.
- Describe - Context - it : [테스트 대상 - 테스트 대상이 놓인 상황 - 테스트 대상이 해당 상황에서 해야하는 행동] 을 순서로 코드를 작성하는 것입니다. 마치.. 시나리오를 작성하는 것처럼??
이 것만 들었을 때에는 아무리 이해심이 높은 사람이라도 이해하기 어려울 것입니다.
테스트 대상을 위한 시나리오를 작성하는 거라고 생각하면 이해가 편하지 않을까 합니다.
위에서 사용했던 상황을 가져와서 살펴봅니다.

- 테스트 대상 → Dialog
- 테스트 대상이 놓인 상황 → 닫기 버튼을 누르면 / 입력된 텍스트가 없다면 등등
- 테스트 대상이 해당 상황에서 해야하는 행동 → title과 description 이 보여야 한다 등등
describe("Dialog", () => {
context("닫기 버튼을 누르면", () => {
it("handleClickClose가 호출되어야 한다.", () => {
renderDialog();
fireEvent.click(screen.getByText("닫기"));
expect(handleClickClose).toBeCalled();
});
});
context("등록버튼을 눌렀을 때", () => {
context("입력된 텍스트가 없다면", () => {
it('console.error에 "입력값이 없는데요.." 문구가 찍혀야 한다.', () => {
renderDialog();
fireEvent.click(screen.getByText("등록"));
expect(spyOnConsoleError).toBeCalledWith("입력값이 없는데요..");
});
});
});
});
코드로 보면 더욱 이해가 갑니다. context가 중첩되는 것도 코드로 따로 빼고 싶은 생각이 드는군요 후후😟 더 알고 싶다면 아래의 글을 추천합니다 좋더군요.
https://kooku0.github.io/blog/프론트엔드에서-테스트코드 짜기/#프론트엔드와-궁합이-잘-맞는-bdd
5-2. 더 생각해봐야 하는 것
- 테스트 코드 자체의 작성을 자동으로 해주는 npm 라이브러리가 있지 않을까? 요즘 AI시대인데..
- 테스트 코드 작성 여부를 코드 리뷰에서 검증하기 (리뷰 자동화도 되면 좋겠다!)
'Develop' 카테고리의 다른 글
lazy loading으로 Entry Point File 크기 줄이는 방법 연구 (0) | 2024.07.20 |
---|---|
React context 대신 Event bus를 사용해 렌더링 개선하는 방법 조사 (0) | 2024.07.20 |
drag event 성능 개선을 위한 JS web worker 사전 조사 (0) | 2024.07.20 |
React : performamce, profiler를 통한 성능 개선 측정하기 (drag & drop, state refactoring) (0) | 2024.07.20 |
쿠키로 마음을 전하고 싶으면 도메인부터 맞추자 🍪 (0) | 2024.07.20 |