Python의 단위 테스트 프레임워크: unittest 완벽 가이드

  • 카카오톡 공유하기
  • 네이버 블로그 공유하기
  • 네이버 밴드에 공유하기
  • 페이스북 공유하기
  • 트위터 공유하기
  • 링크 복사하기

소프트웨어 개발에서 테스트는 코드의 품질을 보장하는 필수적인 과정입니다. Python에서는 표준 라이브러리에 포함된 unittest 프레임워크를 통해 효과적인 단위 테스트를 구현할 수 있습니다. 이 글에서는 unittest의 기본 개념부터 실전 활용법까지 상세히 알아보겠습니다.

unittest란 무엇인가?

unittest는 Python 표준 라이브러리에 포함된 단위 테스트 프레임워크로, Java의 JUnit에서 영감을 받아 설계되었습니다. 이 프레임워크는 테스트 자동화, 테스트 설정 및 종료 코드 공유, 테스트를 컬렉션으로 집계, 테스트와 보고 프레임워크의 독립성 등을 지원합니다.

unittest의 주요 개념

1. 테스트 케이스 (Test Case)

unittest의 기본 단위는 TestCase 클래스입니다. 이 클래스를 상속받아 테스트 메서드를 정의합니다. 각 테스트 메서드는 test_로 시작해야 합니다.

2. 테스트 픽스처 (Test Fixture)

테스트 픽스처는 테스트 실행 전후에 필요한 준비와 정리 작업을 담당합니다. setUp()tearDown() 메서드를 통해 구현합니다.

3. 테스트 스위트 (Test Suite)

여러 테스트 케이스를 그룹화하여 함께 실행할 수 있는 컬렉션입니다.

4. 테스트 러너 (Test Runner)

테스트를 실행하고 결과를 사용자에게 보여주는 컴포넌트입니다.

unittest 기본 사용법

간단한 테스트 케이스 작성하기

다음은 간단한 함수를 테스트하는 unittest 예제입니다:

# 테스트할 함수가 있는 모듈: my_function.py
def add(a, b):
    return a + b

def multiply(a, b):
    return a * b

이제 이 함수들을 테스트하는 코드를 작성해 보겠습니다:

# test3.py
import unittest
from my_function import add, multiply

class TestMyFunctions(unittest.TestCase):
    
    def test_add(self):
        self.assertEqual(add(3, 5), 8)
        self.assertEqual(add(-1, 1), 0)
        self.assertEqual(add(-1, -1), -2)
    
    def test_multiply(self):
        self.assertEqual(multiply(3, 5), 15)
        self.assertEqual(multiply(-1, 1), -1)
        self.assertEqual(multiply(-1, -1), 1)

if __name__ == '__main__':
    unittest.main()

테스트 실행하기

테스트를 실행하는 방법은 두 가지가 있습니다:

  • 파일에서 직접 실행: python test3.py
  • unittest 모듈 사용: python -m unittest test3

테스트 픽스처 활용하기

테스트 전후에 특정 작업을 수행해야 할 때 테스트 픽스처를 활용합니다:

import unittest
import os

class TestFileOperations(unittest.TestCase):
    
    def setUp(self):
        # 테스트 전에 임시 파일 생성
        self.test_file = 'test_file.txt'
        with open(self.test_file, 'w') as f:
            f.write('테스트 데이터')
    
    def tearDown(self):
        # 테스트 후 임시 파일 삭제
        if os.path.exists(self.test_file):
            os.remove(self.test_file)
    
    def test_file_content(self):
        with open(self.test_file, 'r') as f:
            content = f.read()
        self.assertEqual(content, '테스트 데이터')
    
    def test_file_exists(self):
        self.assertTrue(os.path.exists(self.test_file))

if __name__ == '__main__':
    unittest.main()

주요 assertion 메서드

unittest는 다양한 assertion 메서드를 제공합니다:

  • assertEqual(a, b): a와 b가 같은지 확인
  • assertNotEqual(a, b): a와 b가 다른지 확인
  • assertTrue(x): x가 True인지 확인
  • assertFalse(x): x가 False인지 확인
  • assertIs(a, b): a가 b와 동일한 객체인지 확인
  • assertIsNot(a, b): a가 b와 다른 객체인지 확인
  • assertIsNone(x): x가 None인지 확인
  • assertIsNotNone(x): x가 None이 아닌지 확인
  • assertIn(a, b): a가 b에 포함되는지 확인
  • assertNotIn(a, b): a가 b에 포함되지 않는지 확인
  • assertRaises(exc, fun, *args, **kwds): 함수가 예외를 발생시키는지 확인

예외 테스트하기

함수가 특정 상황에서 예외를 발생시키는지 테스트하는 방법은 다음과 같습니다:

def divide(a, b):
    if b == 0:
        raise ValueError("Cannot divide by zero")
    return a / b

class TestDivide(unittest.TestCase):
    
    def test_divide_normal(self):
        self.assertEqual(divide(10, 2), 5)
    
    def test_divide_by_zero(self):
        # 방법 1: with 구문 사용
        with self.assertRaises(ValueError):
            divide(10, 0)
        
        # 방법 2: context manager 사용
        context = self.assertRaises(ValueError)
        with context:
            divide(10, 0)
        self.assertEqual(str(context.exception), "Cannot divide by zero")

테스트 스킵하기

특정 조건에서 테스트를 건너뛰어야 할 때 사용하는 데코레이터입니다:

import unittest
import sys

class TestSkipping(unittest.TestCase):
    
    @unittest.skip("이 테스트는 항상 스킵됩니다")
    def test_always_skipped(self):
        self.fail("이 테스트는 실행되지 않아야 합니다")
    
    @unittest.skipIf(sys.version_info < (3, 9), "Python 3.9 이상에서만 실행")
    def test_python_version(self):
        # Python 3.9 이상에서만 사용 가능한 기능 테스트
        pass
    
    @unittest.skipUnless(sys.platform.startswith("win"), "Windows에서만 실행")
    def test_windows_only(self):
        # Windows 전용 기능 테스트
        pass

테스트 서브클래스

여러 테스트 클래스에서 공통 메서드를 재사용하고 싶을 때 상속을 활용할 수 있습니다:

class BaseTestCase(unittest.TestCase):
    def setUp(self):
        self.base_value = 10
    
    def helper_method(self, value):
        return self.base_value + value

class TestSpecificFeature(BaseTestCase):
    def test_feature(self):
        self.assertEqual(self.helper_method(5), 15)

테스트 스위트 구성하기

여러 테스트를 그룹화하여 실행하고 싶을 때 테스트 스위트를 사용합니다:

import unittest

# 테스트 케이스 클래스들
from test_module1 import TestClass1
from test_module2 import TestClass2, TestClass3

# 테스트 스위트 만들기
def create_test_suite():
    test_suite = unittest.TestSuite()
    
    # 개별 테스트 케이스 추가
    test_suite.addTest(unittest.makeSuite(TestClass1))
    test_suite.addTest(unittest.makeSuite(TestClass2))
    
    # 특정 테스트 메서드만 추가
    test_suite.addTest(TestClass3('test_specific_method'))
    
    return test_suite

if __name__ == '__main__':
    # 테스트 스위트 실행
    runner = unittest.TextTestRunner()
    test_suite = create_test_suite()
    runner.run(test_suite)

unittest.mock 활용하기

외부 의존성이 있는 코드를 테스트할 때 mock 객체를 활용할 수 있습니다:

import unittest
from unittest.mock import Mock, patch

# 테스트할 함수
def get_user_data(user_id, api_client):
    response = api_client.get_user(user_id)
    if response.status_code == 200:
        return response.data
    return None

class TestUserData(unittest.TestCase):
    
    def test_get_user_data_success(self):
        # Mock API 클라이언트 생성
        mock_client = Mock()
        mock_response = Mock()
        mock_response.status_code = 200
        mock_response.data = {'name': 'John', 'age': 30}
        mock_client.get_user.return_value = mock_response
        
        # 함수 테스트
        result = get_user_data(123, mock_client)
        
        # 검증
        self.assertEqual(result, {'name': 'John', 'age': 30})
        mock_client.get_user.assert_called_once_with(123)
    
    def test_get_user_data_failure(self):
        # Mock API 클라이언트 생성
        mock_client = Mock()
        mock_response = Mock()
        mock_response.status_code = 404
        mock_client.get_user.return_value = mock_response
        
        # 함수 테스트
        result = get_user_data(999, mock_client)
        
        # 검증
        self.assertIsNone(result)
        mock_client.get_user.assert_called_once_with(999)
    
    @patch('my_module.api_client')
    def test_with_patch(self, mock_api_client):
        # patch 데코레이터로 모듈 내 객체를 mock으로 대체
        mock_response = Mock()
        mock_response.status_code = 200
        mock_response.data = {'name': 'Jane', 'age': 25}
        mock_api_client.get_user.return_value = mock_response
        
        # 함수 호출 (api_client는 자동으로 mock으로 대체됨)
        from my_module import get_user_data_with_global_client
        result = get_user_data_with_global_client(456)
        
        # 검증
        self.assertEqual(result, {'name': 'Jane', 'age': 25})

테스트 커버리지 확인하기

코드의 어느 부분이 테스트되고 있는지 확인하려면 coverage 패키지를 활용할 수 있습니다:

unittest 모범 사례

테스트 작성 시 고려사항

  • 테스트 독립성 유지: 각 테스트는 다른 테스트에 의존하지 않고 독립적으로 실행될 수 있어야 합니다.
  • 명확한 테스트 이름 사용: 테스트 메서드 이름은 무엇을 테스트하는지 명확히 나타내야 합니다.
  • 하나의 테스트에서는 하나의 동작만 검증: 각 테스트는 단일 기능이나 동작을 검증해야 합니다.
  • 테스트 코드도 유지보수 대상: 테스트 코드도 실제 코드처럼 깔끔하고 유지보수하기 쉽게 작성해야 합니다.
  • 경계 조건 테스트: 정상 케이스뿐만 아니라 경계 조건과 예외 상황도 테스트해야 합니다.

결론

unittest는 Python에서 단위 테스트를 작성하기 위한 강력하고 유연한 프레임워크입니다. 기본적인 테스트 케이스 작성부터 복잡한 테스트 스위트 구성, mock 객체 활용까지 다양한 기능을 제공합니다. 효과적인 테스트 코드 작성은 소프트웨어의 품질을 높이고 버그를 조기에 발견하는 데 큰 도움이 됩니다.

unittest를 활용하여 테스트 주도 개발(TDD)을 실천하거나, 기존 코드의 리팩토링 시 안전망을 구축하는 등 다양한 방식으로 개발 프로세스를 개선해 보세요. 테스트는 단순한 검증 도구를 넘어 더 나은 설계와 구현을 이끌어내는 강력한 도구가 될 수 있습니다.


게시됨

카테고리

작성자

댓글

답글 남기기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다