단위 테스트 (Unit Test)

단위 테스트는 소프트웨어의 가장 작은 단위인 개별 모듈이나 컴포넌트를 독립적으로 테스트하는 과정이다.
이는 마치 자동차를 조립하기 전에 각 부품이 제대로 작동하는지 확인하는 것과 비슷하다.

특징과 목적

단위 테스트의 주요 특징과 목적은 다음과 같다:

  1. 독립성: 각 테스트는 다른 테스트와 독립적으로 실행된다.
  2. 자동화: 테스트를 자동으로 실행할 수 있어 빠르고 반복적인 테스트가 가능하다.
  3. 빠른 피드백: 개발자가 코드를 변경할 때마다 즉시 테스트를 실행하여 문제를 빠르게 발견할 수 있다.
  4. 버그 조기 발견: 개발 초기 단계에서 버그를 찾아 수정 비용을 줄일 수 있다.

테스트 범위

단위 테스트는 주로 다음과 같은 요소를 검증한다다:

  1. 개별 함수나 메서드의 동작
  2. 클래스의 메서드 간 상호작용
  3. 특정 모듈이나 컴포넌트의 기능

수행 시점

단위 테스트는 개발 과정 중 가장 먼저 수행되는 테스트이다.
일반적으로 개발자가 코드를 작성하면서 동시에 또는 바로 후에 테스트를 작성하고 실행한다.

계산기 프로그램의 단위 테스트:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
# 테스트할 계산기 클래스
class Calculator:
    def add(self, a, b):
        return a + b
    
    def subtract(self, a, b):
        return a - b
    
    def multiply(self, a, b):
        return a * b
    
    def divide(self, a, b):
        if b == 0:
            raise ValueError("0으로 나눌 수 없습니다")
        return a / b

# 단위 테스트 코드
import unittest

class TestCalculator(unittest.TestCase):
    def setUp(self):
        # 각 테스트 전에 실행될 준비 코드
        self.calc = Calculator()
    
    def test_add(self):
        # 덧셈 테스트
        self.assertEqual(self.calc.add(2, 3), 5)
        self.assertEqual(self.calc.add(-1, 1), 0)
        self.assertEqual(self.calc.add(0, 0), 0)
    
    def test_divide(self):
        # 나눗셈 테스트
        self.assertEqual(self.calc.divide(6, 2), 3)
        self.assertEqual(self.calc.divide(5, 2), 2.5)
        
        # 예외 상황 테스트
        with self.assertRaises(ValueError):
            self.calc.divide(4, 0)

검증 대상

단위 테스트의 주요 검증 대상은 다음과 같다:

  1. 함수나 메서드의 반환 값
  2. 객체의 상태 변화
  3. 예외 처리의 정확성

단위 테스트의 종류

  1. 상태 기반 테스트: 메서드 실행 후 객체의 상태를 확인한다.

    1
    2
    3
    4
    5
    6
    7
    
    def test_shopping_cart_add_item():
        # 장바구니에 물건을 담았을 때 상태 변화 테스트
        cart = ShoppingCart()
        cart.add_item("노트북", 1)
    
        assert cart.item_count == 1
        assert cart.total_price == 1000000
    
  2. 동작 기반 테스트: 메서드 호출 시 다른 객체와의 상호작용을 확인한다. 보통 mock 객체를 사용한다.

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    
    from unittest.mock import Mock
    
    def test_order_notification():
        # 주문 완료 시 이메일 발송 테스트
        email_service = Mock()
        order_service = OrderService(email_service)
    
        order_service.complete_order("ORDER123")
    
        # 이메일 발송 함수가 호출되었는지 확인
        email_service.send_order_confirmation.assert_called_once()
    

진행 방식

단위 테스트는 보통 다음과 같은 단계로 진행된다:

  1. 테스트 케이스 설계: 검증하고자 하는 동작을 정의한다.

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    
    # 사용자 포인트 시스템 테스트 케이스
    test_cases = {
        "포인트 적립": [
            {"구매금액": 10000, "예상포인트": 100},
            {"구매금액": 0, "예상포인트": 0},
            {"구매금액": -1000, "에러": ValueError}
        ],
        "포인트 사용": [
            {"현재포인트": 1000, "사용포인트": 500, "잔여포인트": 500},
            {"현재포인트": 100, "사용포인트": 200, "에러": ValueError}
        ]
    }
    
  2. 테스트 코드 작성: 주로 AAA (Arrange-Act-Assert) 패턴을 사용한다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
class TestPointSystem(unittest.TestCase):
    def setUp(self):
        self.point_system = PointSystem()
    
    def test_point_accumulation(self):
        for case in test_cases["포인트 적립"]:
            if "에러" in case:
                with self.assertRaises(case["에러"]):
                    self.point_system.accumulate(case["구매금액"])
            else:
                result = self.point_system.accumulate(case["구매금액"])
                self.assertEqual(result, case["예상포인트"])
  1. 테스트 실행: 자동화된 도구를 사용하여 테스트를 실행한다.
  2. 결과 분석: 테스트 결과를 확인하고 필요한 경우 코드를 수정한다.

예시

간단한 계산기 프로그램의 덧셈 기능에 대한 단위 테스트를 예로 들어보자:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
# 계산기 클래스
class Calculator:
    def add(self, a, b):
        return a + b

# 단위 테스트
def test_add():
    calc = Calculator()
    result = calc.add(2, 3)
    assert result == 5, "2 + 3 should equal 5"

# 테스트 실행
test_add()
print("All tests passed!")

이 예시에서 우리는 Calculator 클래스의 add 메서드를 테스트한다.
테스트 함수는 2와 3을 더했을 때 5가 나오는지 확인한다.
이런 식으로 다양한 입력값에 대해 테스트를 작성하여 add 메서드가 정확히 동작하는지 검증할 수 있다.

단위 테스트를 통해 개발자는 자신의 코드가 의도대로 동작하는지 빠르게 확인할 수 있으며, 이는 전체 소프트웨어의 품질을 향상시키는 데 큰 도움이 된다.

소프트웨어의 가장 작은 테스트 가능한 단위(보통 함수나 메서드)를 독립적으로 검증하는 테스트 방법.
이는 코드의 각 부분이 의도한 대로 정확히 작동하는지 확인하는 것을 목적으로 한다.

단위 테스트의 중요한 원칙

  1. 테스트 구조화

    1. 각 테스트는 하나의 동작만 검증한다
    2. 테스트 설정(Setup), 실행(Action), 검증(Assert) 단계가 명확히 구분된다
    3. 테스트 간 의존성이 없도록 설계한다.
  2. 테스트 케이스 설계

    1. 정상 케이스뿐만 아니라 예외 상황도 테스트한다.
      1. 정상 케이스: 예상되는 일반적인 입력
      2. 경계 케이스: 최대값, 최소값, 0 등
      3. 예외 케이스: 잘못된 입력, 에러 상황
    2. 경계값과 특수한 조건들을 고려한다.
    3. 모든 코드 경로를 커버하도록 테스트 케이스를 작성한다.
  3. 코드 품질

    1. 테스트 코드도 실제 코드만큼 깨끗하게 작성되어야 한다.
    2. 테스트의 의도가 명확히 드러나도록 작성한다.
    3. 중복을 최소화하고 재사용 가능한 설정을 활용한다.
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    
    # 테스트 헬퍼 함수를 활용한 코드 중복 제거
    def create_test_user(self, role="user"):
        return User(
            username=f"test_{role}",
            email=f"{role}@example.com",
            role=role
        )
    
    def test_admin_permissions(self):
        admin = self.create_test_user(role="admin")
        assert admin.can_delete_users() == True
    
    def test_user_permissions(self):
        user = self.create_test_user(role="user")
        assert user.can_delete_users() == False
    

참고 및 출처