
단위 테스트란
단위 테스트는 소프트웨어의 기본 구성 단위인 모듈을 테스트하는 과정으로, 모듈 테스트라고도 불린다. 개발자는 구현 단계에서 각 모듈의 개발을 완료한 후, 해당 모듈이 요구사항 명세서에 따라 정확히 구현되었는지 검증한다. 테스트 방식으로는 화이트박스 테스트와 블랙박스 테스트를 모두 사용할 수 있으나, 모듈의 내부 구조를 상세히 검증할 수 있는 화이트박스 테스트가 주로 사용된다.
단위를 격리해서 테스트하기 때문에 아래와 같은 장점이 있다.
- 빠르다. 단위 테스트는 보통 몇 밀리초 내에 수행된다. 테스트 수행 시간이 빠르면 시스템의 큰 부분을 적은 시간을 들여 테스트할 수 있다.
- 다루기 쉽다. 단위 테스트는 어떤 인수를 메서드에 전달하고 메서드의 반환값과 기대하는 결과를 비교하는 식으로 소프트웨어를 테스트한다. 입력값과 기댓값은 테스트에 적용하고 수정하기 쉽다.
- 작성하기 쉽다. 단위 테스트는 복잡한 설정이나 부가적인 작업이 필요 없다. 단일 단위는 응집력이 있고 크기가 작아 테스터가 쉽게 작업할 수 있다.
테스트 수행 시 다음과 같은 보조 도구가 필요할 수 있다.
- 테스트 드라이버$_{test \space driver}$
- 상위 모듈이 개발되지 않은 경우 사용
- 테스트 대상 모듈을 호출하는 가상의 모듈
- 테스트 데이터를 전달하고 결과값을 받는 역할 수행
- 테스트 스텁$_{test \space stub}$
- 하위 모듈이 개발되지 않은 경우 사용
- 테스트 대상 모듈이 호출하는 가상의 모듈
- 전달받은 데이터를 처리하여 결과를 반환하는 역할 수행
단위 테스트를 수행하면 다음과 같은 오류를 발견할 수 있다.
자료형 오류
부적절한 데이터 타입 사용논리 연산자 오류
잘못된 조건식 구성알고리즘 오류
의도하지 않은 결과 도출계산식 오류
잘못된 수식으로 인한 결과값 오류무한 반복 오류
종료 조건이 없는 반복문 사용
프론트엔드에서의 단위 테스트
테스트는 기본적으로 무엇인가 검증하고 확인하는 용도로 만드는 것이므로 어떤 것을 확인할 것인가에 따라 적용하는 테스트가 다르다. 테스트 종류로는 단위 테스트 말고도 스냅샷 테스트, e2e 테스트 등 다양하다. 우선적으로 우리가 적용할 것은 단위 테스트이다.
단위 테스트는 특정 모듈 혹은 단위가 기능을 의도한 대로 올바르게 수행하고 있는지 확인하는 것으로, 테스트 중에서도 가장 기본이고 기저에 깔려 있어야 하는 테스트다. 특정 모듈이나 단위라는 단어가 의미하는 것처럼 단위 테스트는 범위가 한정적이므로 각 단위에 대한 테스트 코드의 양이 적고 효율적이다. 명세가 변경되면 영향을 가장 많이 받기 때문에 설계에 신중해야 한다.
효과적으로 적용하기 위해서는
프로덕트와 코드가 올바르게 동작하고 기능하는지 확인하기 위해 테스트 코드를 작성한다. 코드가 올바르게 작성되었는지 검증하기 위해 사용하는 것이므로, 프로덕트의 안정성과 유지보수성 향상에 기여하는 코드로 대충 쓰는 것이 아닌 잘 작성하려는 노력을 기울여야 한다.
한 번 작성해서 끝내는 것이 아니라, 프로덕트를 구성하는 코드처럼 계속 유지보수되고 발전시켜야 한다. 하지만 완벽한 테스트는 불가능하기 때문에 그만 둘 때를 파악해 적절히 선택을 취하는 것이 좋다.
테스트 작성을 효과적으로 하는 방법
F.I.R.S.T 원칙
단위 테스트가 가져야 할 특성과 원칙에 관해서 이야기하고 있다. 더 나은 프로덕트를 위해 다양한 방법론과 규칙들이 나왔고, 이 원치은 단위 테스트 코드에 대해 더 나은 코드를 만들 수 있는 원칙에 관해 설명한다.
Fast: 단위 테스트는 빨라야 한다.
- 단위 테스트는 내부 코드만 테스트할 때와 외부 자원을 다룰 경우의 실행 시간 차이가 크다. 대상 시스템에 대해 지속적이고 빠르게 피드백을 주는 데 가치가 있기 때문에 빨라야 한다.
- 단위 테스트 실행이 오래 걸린다면 테스트 작성의 의미가 퇴색되는 것이므로 느린 테스트를 지양해야 한다.
Isolated: 단위 테스트는 외부 요인에 종속적이지 않고, 독립적으로 실행해야 한다.
- 테스트하고자 하는 단위 기능을 명확하게 하지 않는다면 그 테스트는 하나 이상의 기능을 테스트할 것이다. 통합 테스트보다 장점을 갖는 것은 하나의 테스트당 하나의 기능만을 테스트하기 때문이다.
Repeatable: 단위 테스트는 실행할 때마다 같은 결과를 만들어야 한다.
- 그렇게 하기 위해서는 결과가 어떻게 나올지 명확해야 하며 통제할 수 있어야 한다. 테스트 대상 코드의 나머지를 격리하고, Mock 객체를 활용하는 방안을 사용해 이를 해결할 수 있다.
Self-validating: 단위 테스트는 스스로 테스트를 통과했는지 아닌지 판단할 수 있어야 한다.
- 테스트 결과를 검증할 때
console.log
를 이용해 출력값에 따라 원하는 값과 비교할 수 있다. 하지만 이렇게 되면 많은 비용을 지불하게 되기 때문에,assert
와 같은 검증 코드를 이용해 검증하도록 한다.
Timely/Thorough: 두 가지 해석이 존재한다고 한다.
- Timely: 단위 테스트는 프로덕션 코드가 테스트에 성공하기 전에 구현되어야 한다(TDD에 적합한 해석).
- 만약 테스트를 제 때 작성하지 않고 미루어 작성한다면 코드에 결함이 발생할 확률이 높아진다.
- Thorough: 단위 테스트는 성공적인 흐름뿐만 아니라, 가능한 모든 에러나 비정상적인 흐름에 대해서도 대응해야 한다.
목적을 명확하게
테스트 코드를 작성할 때는 각 테스트 케이스가 무엇을 검증하는지 명확히 해야 한다. 이를 위해 과일 선호도를 표시하는 간단한 컴포넌트를 예시로 살펴보자.
import { create } from 'zustand'
interface CartItem {
id: string
name: string
quantity: number
}
interface CartState {
items: CartItem[]
updateQuantity: (id: string, quantity: number) => void
}
export const useCartStore = create<CartState>((set) => ({
items: [
{ id: '1', name: '에스프레소', quantity: 1 },
{ id: '2', name: '아메리카노', quantity: 1 },
],
updateQuantity: (id, quantity) =>
set((state) => ({
items: state.items.map((item) =>
item.id === id ? { ...item, quantity } : item
),
})),
}))
import { useCartStore } from './store'
interface Props {
id: string
}
export const CartItem = ({ id }: Props) => {
const item = useCartStore((state) =>
state.items.find((item) => item.id === id)
)
const updateQuantity = useCartStore((state) => state.updateQuantity)
if (!item) return null
return (
<label>
수량:
<select
value={item.quantity}
onChange={(e) => updateQuantity(id, Number(e.target.value))}
aria-label={`${item.name} 수량`}
>
{[1, 2, 3, 4, 5].map((num) => (
<option key={num} value={num}>
{num}
</option>
))}
</select>
</label>
<p className="text-sm">
총 {item.quantity}잔
</p>
)
}
이 컴포넌트는 음료의 수량을 선택하고 총 수량을 표시한다. 이 컴포넌트의 테스트 작성 시 발생할 수 있는 문제점들을 알아보자.
첫 번째 문제는 내부 상태 관리 로직을 직접 테스트하는 것이다. 우리가 테스트하려는 것은 "사용자가 수량 선택 드롭다운에서 3을 선택했을 때, 화면에 '총 3잔'이라고 표시되는가?"이다. 즉 실제 화면에서 일어나는 일을 테스트하고 싶은 것이다.
import { renderHook, act } from '@testing-library/react'
import { useCartStore } from './store'
import { describe, it, expect } from 'vitest'
describe('CartItem', () => {
it('수량이 3으로 변경되는지 확인', () => {
const { result } = renderHook(() => useCartStore())
act(() => {
result.current.updateQuantity('1', 3)
})
expect(result.current.items[0].quantity).toBe(3)
})
})
하지만 이 테스트는 그렇게 하지 않고 상태 저장소를 직접 불러와서(`useCartStore`), 함수를 직접 호출하고(`updateQuantity`), 저장소의 값(`items[0].quantity`)을 확인한다. 이는 마치 "음료 주문 키오스크가 잘 작동하는지 보려고, 화면을 보지 않고 키오스크 내부 회로만 테스트하는 것"과 비슷하다. 사용자는 내부 저장소나 함수를 직접 보지 못하고, 오직 화면에 보이는 것만 볼 수 있다.
또한 컴포넌트의 구현 세부사항에 의존한다. 만약 상태 관리 라이브러리를 Zustand에서 다른 것으로 변경하거나, 상태 관리 방식을 수정하면 테스트 코드도 함께 수정해야 한다. 테스트가 컴포넌트의 실제 사용 방식이 아닌 내부 구현에 결합되어 있다는 뜻이다.
우리가 테스트해야 하는 것은 사용자가 컴포넌트와 상호작용할 때 UI가 올바르게 변경되는지다. 내부적으로 어떤 상태 관리 도구를 사용하는지, 어떤 방식으로 구현했는지는 테스트의 관심사가 아니다.
두 번째 문제는 불충분한 검증이다.
import { render, screen } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { CartItem } from './CartItem'
describe('CartItem', () => {
it('3이라는 숫자가 화면에 표시되는지 확인', async () => {
const user = userEvent.setup()
render(<CartItem id="1" />)
const select = screen.getByRole('combobox')
await user.selectOptions(select, '3')
expect(screen.getByText(/3/)).toBeInTheDocument()
})
})
이 테스트 코드가 가진 문제점을 살펴보자.
현재 코드는 화면에서 단순히 '3'이라는 숫자를 찾는다. 그래서 다음과 같은 상황에서도 테스트가 통과될 수 있다.
- "최대 3잔까지 주문 가능합니다"
- "현재 3명이 주문 대기 중입니다"
- "오늘의 할인: 3% 할인"
`screen.getByText(/3/)` 함수가 정규 표현식을 사용해 화면에서 '3'이라는 텍스트를 찾는다. 정규 표현식 `/3/`은 "3이라는 숫자가 포함된 모든 텍스트"를 찾는다는 의미다.
<div>
<p>최대 3잔까지 주문 가능</p>
<p>총 2잔</p>
</div>
`screen.getByText(/3/)`은 '3'이 포함된 첫 번째 요소를 반환하기에 "최대 3잔까지 주문 가능"이라는 텍스트를 찾게 되고, 이 때문에 실제 우리가 확인하고 싶은 수량과 관계없이 테스트가 통과된다. 테스트 목적에 맞게 ‘내가 작성한 코드의 동작을 올바르게 검증하는 것인가’ 고민하면서 테스트 코드를 작성해야 한다.
여담으로, 더 정확한 테스트를 위해서는
expect(screen.getByText('총 3잔')).toBeInTheDocument()
처럼 문구를 검사해야 한다.
References
https://www.yes24.com/Product/Goods/102487130
https://techblog.woowahan.com/17404/
https://techblog.woowahan.com/17721/
https://www.yes24.com/Product/Goods/117586096
'Web, Front-end > 테스트' 카테고리의 다른 글
Test Double과 객체 - Dummy, Fake, Stub, Mock, Spy (0) | 2025.01.24 |
---|---|
시나리오 구성 및 테스트 코드 작성 (0) | 2025.01.16 |
컴퓨터 전공 관련, 프론트엔드 개발 지식들을 공유합니다. React, Javascript를 다룰 줄 알며 요즘에는 Typescript에도 관심이 생겨 공부하고 있습니다. 서로 소통하면서 프로젝트 하는 것을 즐기며 많은 대외활동으로 개발 능력과 소프트 스킬을 다듬어나가고 있습니다.
포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!