# 기본 예외 처리
try:
number = int(input("숫자를 입력하세요: "))
result = 10 / number
print(f"결과: {result}")
except ValueError:
print("올바른 숫자를 입력해주세요.")
except ZeroDivisionError:
print("0으로 나눌 수 없습니다.")
# 여러 예외를 한 번에 처리
try:
number = int(input("숫자를 입력하세요: "))
result = 10 / number
print(f"결과: {result}")
except (ValueError, ZeroDivisionError) as e:
print(f"오류가 발생했습니다: {e}")
# 모든 예외 처리 (권장하지 않음)
try:
# 위험한 코드
risky_operation()
except Exception as e:
print(f"예상치 못한 오류: {e}")
# 예외 정보 없이 처리
try:
risky_operation()
except:
print("오류가 발생했습니다.") # 어떤 오류인지 알 수 없음
def safe_divide(a, b):
try:
result = a / b
except ZeroDivisionError:
print("0으로 나눌 수 없습니다.")
return None
except TypeError:
print("숫자만 입력 가능합니다.")
return None
else:
# 예외가 발생하지 않았을 때만 실행
print("나눗셈이 성공적으로 완료되었습니다.")
return result
finally:
# 예외 발생 여부와 관계없이 항상 실행
print("나눗셈 연산을 시도했습니다.")
print(safe_divide(10, 2)) # 정상 실행
print(safe_divide(10, 0)) # ZeroDivisionError
print(safe_divide(10, "a")) # TypeError
# 파일 처리에서의 finally 활용
def read_file(filename):
file = None
try:
file = open(filename, 'r', encoding='utf-8')
content = file.read()
return content
except FileNotFoundError:
print(f"파일 '{filename}'을 찾을 수 없습니다.")
return None
except PermissionError:
print(f"파일 '{filename}'에 접근 권한이 없습니다.")
return None
finally:
# 파일이 열려있다면 반드시 닫기
if file and not file.closed:
file.close()
print("파일을 닫았습니다.")
# with문을 사용하면 더 간단 (권장)
def read_file_better(filename):
try:
with open(filename, 'r', encoding='utf-8') as file:
return file.read()
except FileNotFoundError:
print(f"파일 '{filename}'을 찾을 수 없습니다.")
return None
except PermissionError:
print(f"파일 '{filename}'에 접근 권한이 없습니다.")
return None
# with문이 자동으로 파일을 닫아줌
# ValueError: 타입은 맞지만 값이 부적절할 때
try:
number = int("abc") # 문자열을 숫자로 변환 불가
except ValueError as e:
print(f"ValueError: {e}")
try:
import math
result = math.sqrt(-1) # 음수의 제곱근
except ValueError as e:
print(f"ValueError: {e}")
# TypeError: 타입이 맞지 않을 때
try:
result = "hello" + 5 # 문자열과 숫자 더하기 불가
except TypeError as e:
print(f"TypeError: {e}")
try:
numbers = [1, 2, 3]
numbers[1.5] # 인덱스는 정수여야 함
except TypeError as e:
print(f"TypeError: {e}")
# IndexError: 인덱스가 범위를 벗어났을 때
try:
numbers = [1, 2, 3]
print(numbers[5]) # 인덱스 5는 존재하지 않음
except IndexError as e:
print(f"IndexError: {e}")
# KeyError: 딕셔너리에 키가 없을 때
try:
person = {"name": "Alice", "age": 25}
print(person["height"]) # 'height' 키가 없음
except KeyError as e:
print(f"KeyError: {e}")
# AttributeError: 객체에 속성이나 메서드가 없을 때
try:
number = 42
number.append(1) # 정수에는 append 메서드가 없음
except AttributeError as e:
print(f"AttributeError: {e}")
# FileNotFoundError: 파일이 존재하지 않을 때
try:
with open("nonexistent_file.txt", "r") as file:
content = file.read()
except FileNotFoundError as e:
print(f"FileNotFoundError: {e}")
# PermissionError: 권한이 없을 때
try:
with open("/root/restricted_file.txt", "w") as file:
file.write("test")
except PermissionError as e:
print(f"PermissionError: {e}")
# ImportError/ModuleNotFoundError: 모듈을 찾을 수 없을 때
try:
import nonexistent_module
except ImportError as e:
print(f"ImportError: {e}")
# 예외 계층을 이용한 처리
def handle_exceptions():
try:
# 다양한 오류가 발생할 수 있는 코드
user_input = input("숫자를 입력하세요: ")
number = int(user_input)
result = 10 / number
items = [1, 2, 3]
print(items[number])
except ValueError:
print("숫자 형식이 올바르지 않습니다.")
except ZeroDivisionError:
print("0으로 나눌 수 없습니다.")
except IndexError:
print("리스트 인덱스가 범위를 벗어났습니다.")
except ArithmeticError: # ZeroDivisionError의 상위 클래스
print("산술 연산 오류가 발생했습니다.")
except LookupError: # IndexError, KeyError의 상위 클래스
print("조회 오류가 발생했습니다.")
except Exception as e: # 모든 예외의 상위 클래스
print(f"예상치 못한 오류: {e}")
# 예외 타입 확인
def check_exception_type():
try:
result = 10 / 0
except Exception as e:
print(f"예외 타입: {type(e).__name__}")
print(f"예외 메시지: {str(e)}")
print(f"예외 args: {e.args}")
# 특정 예외인지 확인
if isinstance(e, ZeroDivisionError):
print("이것은 ZeroDivisionError입니다.")
check_exception_type()
# 기본 커스텀 예외
class CustomError(Exception):
"""사용자 정의 예외"""
pass
class InvalidAgeError(Exception):
"""나이가 유효하지 않을 때 발생하는 예외"""
def __init__(self, age, message="나이가 유효하지 않습니다"):
self.age = age
self.message = message
super().__init__(self.message)
def __str__(self):
return f"{self.message}: {self.age}"
class BankAccountError(Exception):
"""은행 계좌 관련 예외의 기본 클래스"""
pass
class InsufficientFundsError(BankAccountError):
"""잔액 부족 시 발생하는 예외"""
def __init__(self, balance, amount):
self.balance = balance
self.amount = amount
message = f"잔액 부족: 현재 잔액 {balance}원, 요청 금액 {amount}원"
super().__init__(message)
class InvalidAccountError(BankAccountError):
"""잘못된 계좌번호일 때 발생하는 예외"""
pass
# 커스텀 예외 사용 예시
class Person:
def __init__(self, name, age):
self.name = name
if not self.is_valid_age(age):
raise InvalidAgeError(age)
self.age = age
@staticmethod
def is_valid_age(age):
return isinstance(age, int) and 0 <= age <= 150
class BankAccount:
def __init__(self, account_number, initial_balance=0):
self.account_number = account_number
self.balance = initial_balance
def withdraw(self, amount):
if amount > self.balance:
raise InsufficientFundsError(self.balance, amount)
self.balance -= amount
return self.balance
def deposit(self, amount):
if amount <= 0:
raise ValueError("입금 금액은 0보다 커야 합니다")
self.balance += amount
return self.balance
# 사용 예시
try:
person = Person("Alice", -5)
except InvalidAgeError as e:
print(f"Person 생성 실패: {e}")
try:
account = BankAccount("123456", 1000)
account.withdraw(1500)
except InsufficientFundsError as e:
print(f"출금 실패: {e}")
except BankAccountError as e:
print(f"계좌 오류: {e}")
try:
account.deposit(-100)
except ValueError as e:
print(f"입금 실패: {e}")
# 예외 체이닝: 원래 예외를 유지하면서 새 예외 발생
class DataProcessingError(Exception):
"""데이터 처리 중 발생하는 예외"""
pass
def process_data(data):
try:
# 데이터 처리 중 오류 발생
result = int(data) / 0
return result
except (ValueError, ZeroDivisionError) as e:
# 원래 예외를 체이닝하여 새 예외 발생
raise DataProcessingError("데이터 처리 중 오류가 발생했습니다") from e
def advanced_processing(data):
try:
return process_data(data)
except DataProcessingError as e:
print(f"고급 처리 실패: {e}")
print(f"원인: {e.__cause__}")
print(f"원인 타입: {type(e.__cause__).__name__}")
# 예외 체이닝 테스트
try:
advanced_processing("invalid")
except Exception as e:
print(f"\n전체 예외 체인:")
current = e
while current:
print(f"- {type(current).__name__}: {current}")
current = current.__cause__
# 나쁜 예: 너무 광범위한 예외 처리
def bad_example():
try:
# 여러 가지 일을 함
user_input = input("숫자 입력: ")
number = int(user_input)
result = 10 / number
with open("data.txt", "r") as f:
data = f.read()
return result, data
except Exception: # 모든 예외를 잡음 - 나쁜 방법
print("뭔가 잘못되었습니다")
return None
# 좋은 예: 구체적인 예외 처리
def good_example():
try:
user_input = input("숫자 입력: ")
number = int(user_input)
except ValueError:
print("유효한 숫자를 입력해주세요.")
return None
try:
result = 10 / number
except ZeroDivisionError:
print("0으로 나눌 수 없습니다.")
return None
try:
with open("data.txt", "r") as f:
data = f.read()
except FileNotFoundError:
print("data.txt 파일을 찾을 수 없습니다.")
return None
except PermissionError:
print("파일 읽기 권한이 없습니다.")
return None
return result, data
# 더 좋은 예: 함수 분리
def get_user_number():
"""사용자로부터 숫자 입력 받기"""
while True:
try:
user_input = input("숫자 입력: ")
return int(user_input)
except ValueError:
print("유효한 숫자를 입력해주세요.")
except KeyboardInterrupt:
print("\n프로그램을 종료합니다.")
return None
def safe_divide(dividend, divisor):
"""안전한 나눗셈"""
try:
return dividend / divisor
except ZeroDivisionError:
raise ValueError("0으로 나눌 수 없습니다.")
def read_data_file(filename):
"""데이터 파일 읽기"""
try:
with open(filename, "r", encoding="utf-8") as f:
return f.read()
except FileNotFoundError:
raise FileNotFoundError(f"'{filename}' 파일을 찾을 수 없습니다.")
except PermissionError:
raise PermissionError(f"'{filename}' 파일 읽기 권한이 없습니다.")
import logging
import traceback
# 로깅 설정
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(levelname)s - %(message)s',
handlers=[
logging.FileHandler('app.log'),
logging.StreamHandler()
]
)
logger = logging.getLogger(__name__)
def divide_with_logging(a, b):
"""로깅을 포함한 나눗셈"""
try:
logger.info(f"나눗셈 시도: {a} / {b}")
result = a / b
logger.info(f"나눗셈 성공: {result}")
return result
except ZeroDivisionError as e:
logger.error(f"0으로 나누기 시도: {a} / {b}")
logger.error(f"예외 상세: {e}")
raise
except Exception as e:
logger.critical(f"예상치 못한 오류: {e}")
logger.critical(f"스택 트레이스:\n{traceback.format_exc()}")
raise
def process_user_data(user_data):
"""사용자 데이터 처리"""
try:
# 데이터 검증
if not isinstance(user_data, dict):
raise TypeError("사용자 데이터는 딕셔너리여야 합니다")
required_fields = ['name', 'age', 'email']
for field in required_fields:
if field not in user_data:
raise ValueError(f"필수 필드 '{field}'가 없습니다")
# 데이터 처리
age = int(user_data['age'])
if age < 0 or age > 150:
raise ValueError(f"유효하지 않은 나이: {age}")
logger.info(f"사용자 데이터 처리 완료: {user_data['name']}")
return True
except (TypeError, ValueError) as e:
logger.warning(f"데이터 검증 실패: {e}")
logger.warning(f"입력 데이터: {user_data}")
return False
except Exception as e:
logger.error(f"예상치 못한 오류: {e}")
logger.error(f"트레이스백:\n{traceback.format_exc()}")
return False
# 디버깅을 위한 예외 정보 출력
def debug_exception():
try:
numbers = [1, 2, 3]
print(numbers[10])
except Exception as e:
print("=== 예외 디버깅 정보 ===")
print(f"예외 타입: {type(e).__name__}")
print(f"예외 메시지: {str(e)}")
print(f"예외 args: {e.args}")
print("\n=== 스택 트레이스 ===")
traceback.print_exc()
print("\n=== 포맷된 트레이스백 ===")
print(traceback.format_exc())
# 사용 예시
try:
result = divide_with_logging(10, 0)
except ZeroDivisionError:
print("나눗셈 실패를 처리했습니다.")
# 잘못된 데이터로 테스트
invalid_data = {"name": "Alice", "age": "invalid"}
process_user_data(invalid_data)
debug_exception()
# 커스텀 컨텍스트 매니저
class DatabaseConnection:
def __init__(self, connection_string):
self.connection_string = connection_string
self.connection = None
def __enter__(self):
print(f"데이터베이스 연결 중: {self.connection_string}")
# 실제로는 데이터베이스 연결 코드
self.connection = f"Connected to {self.connection_string}"
return self.connection
def __exit__(self, exc_type, exc_value, traceback):
print("데이터베이스 연결 해제 중...")
if exc_type is not None:
print(f"예외가 발생했습니다: {exc_type.__name__}: {exc_value}")
# 예외를 로깅하거나 정리 작업 수행
return False # 예외를 다시 발생시킴
print("정상적으로 연결 해제됨")
return True
# 사용 예시
try:
with DatabaseConnection("postgresql://localhost:5432/mydb") as conn:
print(f"연결 상태: {conn}")
# 데이터베이스 작업
raise ValueError("데이터베이스 쿼리 오류")
except ValueError as e:
print(f"처리된 예외: {e}")
# 파일 처리를 위한 안전한 컨텍스트 매니저
class SafeFileManager:
def __init__(self, filename, mode='r', encoding='utf-8'):
self.filename = filename
self.mode = mode
self.encoding = encoding
self.file = None
def __enter__(self):
try:
self.file = open(self.filename, self.mode, encoding=self.encoding)
return self.file
except FileNotFoundError:
print(f"파일 '{self.filename}'을 찾을 수 없어 새로 생성합니다.")
self.file = open(self.filename, 'w', encoding=self.encoding)
return self.file
except PermissionError:
print(f"파일 '{self.filename}'에 대한 권한이 없습니다.")
raise
def __exit__(self, exc_type, exc_value, traceback):
if self.file and not self.file.closed:
self.file.close()
print(f"파일 '{self.filename}' 닫기 완료")
if exc_type is not None:
print(f"파일 작업 중 오류 발생: {exc_type.__name__}")
return False
# 사용 예시
with SafeFileManager('test.txt', 'w') as f:
f.write("테스트 내용입니다.")
# 여러 예외를 동시에 처리하는 함수
def robust_file_processor(filename):
"""강건한 파일 처리기"""
try:
with open(filename, 'r', encoding='utf-8') as file:
lines = file.readlines()
# 각 줄을 숫자로 변환 시도
numbers = []
for i, line in enumerate(lines, 1):
try:
number = float(line.strip())
numbers.append(number)
except ValueError:
logger.warning(f"줄 {i}: '{line.strip()}'은 숫자가 아닙니다. 건너뜁니다.")
continue
if not numbers:
raise ValueError("유효한 숫자가 하나도 없습니다.")
# 통계 계산
total = sum(numbers)
average = total / len(numbers)
return {
'count': len(numbers),
'total': total,
'average': average,
'min': min(numbers),
'max': max(numbers)
}
except FileNotFoundError:
logger.error(f"파일 '{filename}'을 찾을 수 없습니다.")
return None
except PermissionError:
logger.error(f"파일 '{filename}' 읽기 권한이 없습니다.")
return None
except ValueError as e:
logger.error(f"데이터 처리 오류: {e}")
return None
except Exception as e:
logger.critical(f"예상치 못한 오류: {e}")
logger.critical(traceback.format_exc())
return None
# 테스트 데이터 파일 생성
with open('numbers.txt', 'w') as f:
f.write("10.5\n20.3\ninvalid\n30.7\n40.2\n")
# 파일 처리 테스트
result = robust_file_processor('numbers.txt')
if result:
print("파일 처리 결과:")
for key, value in result.items():
print(f" {key}: {value}")
import unittest
# 테스트할 함수들
def add(a, b):
"""두 수를 더합니다."""
return a + b
def divide(a, b):
"""나눗셈을 수행합니다."""
if b == 0:
raise ValueError("0으로 나눌 수 없습니다.")
return a / b
def is_even(number):
"""숫자가 짝수인지 확인합니다."""
return number % 2 == 0
class Calculator:
"""간단한 계산기 클래스"""
def __init__(self):
self.history = []
def add(self, a, b):
result = a + b
self.history.append(f"{a} + {b} = {result}")
return result
def get_history(self):
return self.history.copy()
def clear_history(self):
self.history.clear()
# 단위 테스트 클래스
class TestMathFunctions(unittest.TestCase):
"""수학 함수들에 대한 테스트"""
def test_add_positive_numbers(self):
"""양수 덧셈 테스트"""
self.assertEqual(add(2, 3), 5)
self.assertEqual(add(10, 15), 25)
def test_add_negative_numbers(self):
"""음수 덧셈 테스트"""
self.assertEqual(add(-2, -3), -5)
self.assertEqual(add(-10, 5), -5)
def test_add_zero(self):
"""0과의 덧셈 테스트"""
self.assertEqual(add(5, 0), 5)
self.assertEqual(add(0, 0), 0)
def test_divide_normal(self):
"""정상적인 나눗셈 테스트"""
self.assertEqual(divide(10, 2), 5)
self.assertAlmostEqual(divide(10, 3), 3.333333, places=5)
def test_divide_by_zero(self):
"""0으로 나누기 예외 테스트"""
with self.assertRaises(ValueError):
divide(10, 0)
with self.assertRaises(ValueError) as context:
divide(5, 0)
self.assertEqual(str(context.exception), "0으로 나눌 수 없습니다.")
def test_is_even(self):
"""짝수 판별 테스트"""
self.assertTrue(is_even(2))
self.assertTrue(is_even(0))
self.assertTrue(is_even(-4))
self.assertFalse(is_even(1))
self.assertFalse(is_even(-3))
class TestCalculator(unittest.TestCase):
"""계산기 클래스 테스트"""
def setUp(self):
"""각 테스트 전에 실행되는 설정"""
self.calc = Calculator()
def tearDown(self):
"""각 테스트 후에 실행되는 정리"""
# 여기서는 특별한 정리 작업이 없음
pass
def test_calculator_add(self):
"""계산기 덧셈 테스트"""
result = self.calc.add(5, 3)
self.assertEqual(result, 8)
def test_history_tracking(self):
"""히스토리 추적 테스트"""
self.calc.add(2, 3)
self.calc.add(5, 7)
history = self.calc.get_history()
self.assertEqual(len(history), 2)
self.assertIn("2 + 3 = 5", history)
self.assertIn("5 + 7 = 12", history)
def test_clear_history(self):
"""히스토리 클리어 테스트"""
self.calc.add(1, 1)
self.calc.clear_history()
self.assertEqual(len(self.calc.get_history()), 0)
class TestDataTypes(unittest.TestCase):
"""다양한 데이터 타입 테스트"""
def test_list_operations(self):
"""리스트 연산 테스트"""
my_list = [1, 2, 3]
my_list.append(4)
self.assertIn(4, my_list)
self.assertNotIn(5, my_list)
self.assertEqual(len(my_list), 4)
def test_string_operations(self):
"""문자열 연산 테스트"""
text = "Hello World"
self.assertGreater(len(text), 5)
self.assertLess(len(text), 20)
self.assertRegex(text, r"Hello.*")
self.assertNotRegex(text, r"\d+")
def test_dictionary_operations(self):
"""딕셔너리 연산 테스트"""
person = {"name": "Alice", "age": 25}
self.assertDictEqual(person, {"name": "Alice", "age": 25})
self.assertEqual(person["name"], "Alice")
# 키 존재 여부 테스트
with self.assertRaises(KeyError):
_ = person["height"]
# 클래스 레벨 설정
class TestWithClassSetup(unittest.TestCase):
"""클래스 레벨 설정/해제를 사용하는 테스트"""
@classmethod
def setUpClass(cls):
"""모든 테스트 전에 한 번만 실행"""
print("클래스 설정: 테스트 시작")
cls.shared_resource = "공유 리소스"
@classmethod
def tearDownClass(cls):
"""모든 테스트 후에 한 번만 실행"""
print("클래스 해제: 테스트 완료")
def test_shared_resource(self):
"""공유 리소스 테스트"""
self.assertEqual(self.shared_resource, "공유 리소스")
# 테스트 실행
if __name__ == '__main__':
# 모든 테스트 실행
unittest.main(verbosity=2)
# 특정 테스트만 실행하려면:
# suite = unittest.TestLoader().loadTestsFromTestCase(TestMathFunctions)
# unittest.TextTestRunner(verbosity=2).run(suite)
import unittest
from unittest.mock import Mock, patch, MagicMock
import tempfile
import os
# 외부 의존성이 있는 함수들
def get_weather_data(city):
"""외부 API에서 날씨 데이터를 가져오는 함수 (시뮬레이션)"""
import requests # 실제로는 requests 라이브러리 사용
response = requests.get(f"http://api.weather.com/{city}")
return response.json()
def save_to_file(data, filename):
"""파일에 데이터 저장"""
with open(filename, 'w') as f:
f.write(str(data))
def read_from_file(filename):
"""파일에서 데이터 읽기"""
with open(filename, 'r') as f:
return f.read()
class DataProcessor:
"""데이터 처리 클래스"""
def __init__(self, api_client):
self.api_client = api_client
def process_user_data(self, user_id):
raw_data = self.api_client.get_user(user_id)
return {
'id': raw_data['id'],
'name': raw_data['name'].upper(),
'email_domain': raw_data['email'].split('@')[1]
}
class TestAdvancedTechniques(unittest.TestCase):
"""고급 테스트 기법들"""
def test_with_temporary_file(self):
"""임시 파일을 사용한 테스트"""
with tempfile.NamedTemporaryFile(mode='w', delete=False) as tf:
tf.write("테스트 데이터")
temp_filename = tf.name
try:
# 파일 읽기 테스트
content = read_from_file(temp_filename)
self.assertEqual(content, "테스트 데이터")
finally:
# 임시 파일 정리
os.unlink(temp_filename)
@patch('requests.get')
def test_weather_api_mock(self, mock_get):
"""외부 API 모킹 테스트"""
# Mock 응답 설정
mock_response = Mock()
mock_response.json.return_value = {
'temperature': 25,
'humidity': 60,
'condition': 'sunny'
}
mock_get.return_value = mock_response
# 함수 테스트
result = get_weather_data('Seoul')
# 검증
self.assertEqual(result['temperature'], 25)
mock_get.assert_called_once_with("http://api.weather.com/Seoul")
def test_data_processor_with_mock(self):
"""Mock 객체를 사용한 의존성 테스트"""
# Mock API 클라이언트 생성
mock_api_client = Mock()
mock_api_client.get_user.return_value = {
'id': 123,
'name': 'alice',
'email': '[email protected]'
}
# 테스트 실행
processor = DataProcessor(mock_api_client)
result = processor.process_user_data(123)
# 검증
expected = {
'id': 123,
'name': 'ALICE',
'email_domain': 'example.com'
}
self.assertEqual(result, expected)
mock_api_client.get_user.assert_called_once_with(123)
def test_exception_message(self):
"""예외 메시지 상세 테스트"""
with self.assertRaisesRegex(ValueError, r"0으로 나눌 수 없습니다"):
divide(10, 0)
def test_multiple_assertions(self):
"""여러 조건을 한 번에 테스트"""
with self.subTest("양수 테스트"):
self.assertGreater(10, 0)
with self.subTest("음수 테스트"):
self.assertLess(-5, 0)
with self.subTest("0 테스트"):
self.assertEqual(0, 0)
@unittest.skip("아직 구현되지 않은 기능")
def test_future_feature(self):
"""향후 구현될 기능 테스트"""
pass
@unittest.skipIf(os.name == 'nt', "Windows에서는 실행하지 않음")
def test_unix_specific(self):
"""Unix 전용 테스트"""
self.assertTrue(True)
def test_performance(self):
"""성능 테스트 예시"""
import time
start_time = time.time()
# 테스트할 코드
result = sum(range(10000))
end_time = time.time()
execution_time = end_time - start_time
# 성능 검증 (1초 이내에 완료되어야 함)
self.assertLess(execution_time, 1.0)
self.assertEqual(result, 49995000)
# 테스트 스위트 구성
def create_test_suite():
"""사용자 정의 테스트 스위트"""
suite = unittest.TestSuite()
# 특정 테스트만 추가
suite.addTest(TestMathFunctions('test_add_positive_numbers'))
suite.addTest(TestCalculator('test_calculator_add'))
suite.addTest(TestAdvancedTechniques('test_data_processor_with_mock'))
return suite
if __name__ == '__main__':
# 일반 테스트 실행
unittest.main(verbosity=2, exit=False)
# 커스텀 테스트 스위트 실행
print("\n=== 커스텀 테스트 스위트 ===")
runner = unittest.TextTestRunner(verbosity=2)
runner.run(create_test_suite())
# pytest 설치: pip install pytest
# test_with_pytest.py
import pytest
# 기본 테스트 함수들 (test_로 시작해야 함)
def test_add():
"""기본 덧셈 테스트"""
assert add(2, 3) == 5
assert add(-1, 1) == 0
def test_divide():
"""나눗셈 테스트"""
assert divide(10, 2) == 5
assert divide(9, 3) == 3
def test_divide_by_zero():
"""0으로 나누기 예외 테스트"""
with pytest.raises(ValueError):
divide(10, 0)
with pytest.raises(ValueError, match="0으로 나눌 수 없습니다"):
divide(5, 0)
# 픽스처 사용
@pytest.fixture
def calculator():
"""계산기 픽스처"""
return Calculator()
@pytest.fixture
def sample_data():
"""샘플 데이터 픽스처"""
return [1, 2, 3, 4, 5]
def test_calculator_with_fixture(calculator):
"""픽스처를 사용한 계산기 테스트"""
result = calculator.add(5, 3)
assert result == 8
assert len(calculator.get_history()) == 1
def test_data_processing(sample_data):
"""샘플 데이터를 사용한 테스트"""
assert len(sample_data) == 5
assert sum(sample_data) == 15
assert max(sample_data) == 5
# 파라미터화된 테스트
@pytest.mark.parametrize("a,b,expected", [
(2, 3, 5),
(10, 15, 25),
(-2, -3, -5),
(0, 5, 5),
])
def test_add_parametrized(a, b, expected):
"""파라미터화된 덧셈 테스트"""
assert add(a, b) == expected
@pytest.mark.parametrize("number,expected", [
(2, True),
(3, False),
(0, True),
(-4, True),
(-3, False),
])
def test_is_even_parametrized(number, expected):
"""파라미터화된 짝수 판별 테스트"""
assert is_even(number) == expected
# 마크를 사용한 테스트 분류
@pytest.mark.slow
def test_slow_operation():
"""느린 작업 테스트"""
import time
time.sleep(0.1) # 시뮬레이션
assert True
@pytest.mark.integration
def test_integration():
"""통합 테스트"""
assert True
@pytest.mark.unit
def test_unit():
"""단위 테스트"""
assert True
# 스킵과 조건부 실행
@pytest.mark.skip(reason="아직 구현되지 않음")
def test_not_implemented():
"""구현되지 않은 기능"""
pass
@pytest.mark.skipif(os.name == 'nt', reason="Windows에서는 실행하지 않음")
def test_unix_only():
"""Unix 전용 테스트"""
assert True
# 예상 실패
@pytest.mark.xfail(reason="알려진 버그")
def test_known_bug():
"""알려진 버그가 있는 테스트"""
assert False
# 클래스 기반 테스트
class TestCalculatorClass:
"""계산기 클래스 테스트"""
@pytest.fixture(autouse=True)
def setup(self):
"""자동으로 실행되는 설정"""
self.calc = Calculator()
def test_add(self):
assert self.calc.add(2, 3) == 5
def test_history(self):
self.calc.add(1, 1)
assert len(self.calc.get_history()) == 1
# conftest.py 파일에 공통 픽스처 정의 (별도 파일)
# @pytest.fixture(scope="session")
# def database():
# """데이터베이스 연결 픽스처 (세션 전체에서 한 번만 실행)"""
# db = create_test_database()
# yield db
# db.close()
# pytest 실행 방법:
# pytest test_with_pytest.py # 모든 테스트 실행
# pytest test_with_pytest.py::test_add # 특정 테스트 실행
# pytest -v # 상세 출력
# pytest -k "add" # 이름에 'add'가 포함된 테스트만
# pytest -m "slow" # 'slow' 마크가 있는 테스트만
# pytest --tb=short # 짧은 트레이스백
# pytest --cov=mymodule # 커버리지 측정 (pytest-cov 설치 필요)
# conftest.py (프로젝트 루트에 위치)
import pytest
import tempfile
import shutil
from pathlib import Path
# 세션 스코프 픽스처 (한 번만 실행)
@pytest.fixture(scope="session")
def test_data_dir():
"""테스트 데이터 디렉토리 생성"""
temp_dir = tempfile.mkdtemp()
yield Path(temp_dir)
shutil.rmtree(temp_dir)
# 모듈 스코프 픽스처 (모듈당 한 번 실행)
@pytest.fixture(scope="module")
def database_connection():
"""데이터베이스 연결 시뮬레이션"""
class MockDB:
def __init__(self):
self.data = {}
def insert(self, key, value):
self.data[key] = value
def get(self, key):
return self.data.get(key)
def close(self):
self.data.clear()
db = MockDB()
yield db
db.close()
# 함수 스코프 픽스처 (각 테스트마다 실행)
@pytest.fixture
def user_data():
"""사용자 데이터 픽스처"""
return {
"name": "Test User",
"email": "[email protected]",
"age": 30
}
# 파라미터화된 픽스처
@pytest.fixture(params=["sqlite", "mysql", "postgresql"])
def db_type(request):
"""다양한 데이터베이스 타입"""
return request.param
# 조건부 픽스처
@pytest.fixture
def temp_file(tmp_path):
"""임시 파일 생성"""
file_path = tmp_path / "test_file.txt"
file_path.write_text("테스트 내용")
return file_path
# test_advanced_pytest.py
import pytest
from unittest.mock import Mock, patch
import json
import requests
# 픽스처 조합 사용
def test_database_operations(database_connection, user_data):
"""데이터베이스 연산 테스트"""
db = database_connection
# 데이터 삽입
db.insert("user1", user_data)
# 데이터 조회
retrieved = db.get("user1")
assert retrieved == user_data
assert retrieved["name"] == "Test User"
# 픽스처를 사용한 파일 테스트
def test_file_operations(temp_file):
"""파일 연산 테스트"""
content = temp_file.read_text()
assert content == "테스트 내용"
# 파일 수정
temp_file.write_text("수정된 내용")
new_content = temp_file.read_text()
assert new_content == "수정된 내용"
# 모킹을 사용한 테스트
@patch('requests.get')
def test_api_call_with_mock(mock_get):
"""API 호출 모킹 테스트"""
# Mock 응답 설정
mock_response = Mock()
mock_response.json.return_value = {"status": "success", "data": "test"}
mock_response.status_code = 200
mock_get.return_value = mock_response
# 테스트할 함수
def fetch_data(url):
response = requests.get(url)
return response.json()
# 테스트 실행
result = fetch_data("https://api.example.com/data")
# 검증
assert result["status"] == "success"
mock_get.assert_called_once_with("https://api.example.com/data")
# 예외 테스트의 다양한 방법
def test_exception_details():
"""예외 상세 정보 테스트"""
with pytest.raises(ValueError) as exc_info:
raise ValueError("상세한 오류 메시지")
assert "상세한 오류 메시지" in str(exc_info.value)
assert exc_info.type == ValueError
# 여러 케이스를 한 번에 테스트
@pytest.mark.parametrize("input_data,expected", [
({"name": "Alice", "age": 25}, True),
({"name": "", "age": 25}, False),
({"name": "Bob", "age": -1}, False),
({"name": "Charlie"}, False), # age 키 없음
])
def test_validate_user_data(input_data, expected):
"""사용자 데이터 검증 테스트"""
def validate_user(data):
if not data.get("name") or not isinstance(data.get("age"), int):
return False
return data["age"] > 0
assert validate_user(input_data) == expected
# 커스텀 마크 정의
pytestmark = pytest.mark.api # 파일 전체에 마크 적용
@pytest.mark.slow
@pytest.mark.integration
def test_complex_integration():
"""복잡한 통합 테스트"""
# 시간이 오래 걸리는 통합 테스트
import time
time.sleep(0.1)
assert True
# 테스트 데이터 생성기
@pytest.fixture
def user_factory():
"""사용자 데이터 팩토리"""
def create_user(name="Default", age=25, email=None):
email = email or f"{name.lower()}@example.com"
return {
"name": name,
"age": age,
"email": email
}
return create_user
def test_user_factory(user_factory):
"""사용자 팩토리 테스트"""
user1 = user_factory("Alice", 30)
user2 = user_factory("Bob")
assert user1["name"] == "Alice"
assert user1["age"] == 30
assert user2["email"] == "[email protected]"
# 테스트 클래스 with 픽스처
class TestUserService:
"""사용자 서비스 테스트 클래스"""
@pytest.fixture(autouse=True)
def setup_method(self, database_connection):
"""각 테스트 메서드 전에 자동 실행"""
self.db = database_connection
self.service = UserService(self.db)
def test_create_user(self, user_data):
"""사용자 생성 테스트"""
user_id = self.service.create_user(user_data)
assert user_id is not None
retrieved = self.service.get_user(user_id)
assert retrieved["name"] == user_data["name"]
def test_update_user(self, user_data):
"""사용자 업데이트 테스트"""
user_id = self.service.create_user(user_data)
update_data = {"name": "Updated Name"}
self.service.update_user(user_id, update_data)
updated = self.service.get_user(user_id)
assert updated["name"] == "Updated Name"
# UserService 클래스 (테스트 대상)
class UserService:
def __init__(self, db):
self.db = db
self._counter = 0
def create_user(self, user_data):
self._counter += 1
user_id = f"user_{self._counter}"
self.db.insert(user_id, user_data.copy())
return user_id
def get_user(self, user_id):
return self.db.get(user_id)
def update_user(self, user_id, update_data):
user = self.db.get(user_id)
if user:
user.update(update_data)
self.db.insert(user_id, user)
# 조건부 테스트 스킵
import sys
@pytest.mark.skipif(sys.version_info < (3, 8), reason="Python 3.8+ 필요")
def test_walrus_operator():
"""바다코끼리 연산자 테스트 (Python 3.8+)"""
data = [1, 2, 3, 4, 5]
if (n := len(data)) > 3:
assert n == 5
# 실패 예상 테스트
@pytest.mark.xfail(reason="알려진 버그, 다음 릴리스에서 수정 예정")
def test_known_failure():
"""알려진 실패 테스트"""
assert 1 == 2 # 의도적으로 실패
@pytest.mark.xfail(strict=True) # strict=True면 패스하면 실패로 처리
def test_strict_xfail():
"""엄격한 실패 예상 테스트"""
assert False
# 테스트 데이터 파일 사용
def test_json_data(tmp_path):
"""JSON 데이터 파일 테스트"""
# 테스트 데이터 파일 생성
test_data = {
"users": [
{"name": "Alice", "age": 25},
{"name": "Bob", "age": 30}
]
}
json_file = tmp_path / "test_data.json"
json_file.write_text(json.dumps(test_data))
# 파일에서 데이터 읽기 테스트
with open(json_file) as f:
loaded_data = json.load(f)
assert len(loaded_data["users"]) == 2
assert loaded_data["users"][0]["name"] == "Alice"
# 성능 테스트
def test_performance_benchmark(benchmark):
"""성능 벤치마크 테스트 (pytest-benchmark 설치 필요)"""
def sort_function():
data = list(range(1000, 0, -1))
return sorted(data)
result = benchmark(sort_function)
assert len(result) == 1000
assert result[0] == 1
# 병렬 테스트를 위한 설정
# pytest -n auto (pytest-xdist 설치 필요)
# pytest -n 4 (4개 프로세스로 병렬 실행)
# 커버리지 테스트
# pytest --cov=mymodule --cov-report=html
# pytest.ini 설정 파일 예시 (프로젝트 루트에 생성)
"""
[tool:pytest]
minversion = 6.0
addopts = -ra -q --tb=short
testpaths = tests
python_files = test_*.py *_test.py
python_classes = Test*
python_functions = test_*
markers =
slow: marks tests as slow
integration: marks tests as integration tests
unit: marks tests as unit tests
api: marks tests as API tests
"""
# 플러그인 추천
"""
유용한 pytest 플러그인들:
1. pytest-cov: 코드 커버리지 측정
pip install pytest-cov
pytest --cov=mymodule
2. pytest-xdist: 병렬 테스트 실행
pip install pytest-xdist
pytest -n auto
3. pytest-mock: 더 쉬운 모킹
pip install pytest-mock
4. pytest-benchmark: 성능 벤치마크
pip install pytest-benchmark
5. pytest-html: HTML 리포트 생성
pip install pytest-html
pytest --html=report.html
6. pytest-watch: 파일 변경 시 자동 테스트
pip install pytest-watch
ptw
7. pytest-sugar: 더 예쁜 출력
pip install pytest-sugar
8. pytest-asyncio: 비동기 테스트
pip install pytest-asyncio
"""
# 좋은 테스트 구조 예시
# tests/conftest.py - 공통 픽스처와 설정
import pytest
from myapp.database import create_test_db
from myapp.models import User
from myapp.services import UserService
@pytest.fixture(scope="session")
def db():
"""테스트 데이터베이스"""
database = create_test_db()
yield database
database.close()
@pytest.fixture
def user_service(db):
"""사용자 서비스"""
return UserService(db)
@pytest.fixture
def sample_user():
"""샘플 사용자 데이터"""
return {
"username": "testuser",
"email": "[email protected]",
"age": 25
}
# tests/test_models.py - 모델 테스트
import pytest
from myapp.models import User, ValidationError
class TestUser:
"""사용자 모델 테스트"""
def test_user_creation_success(self, sample_user):
"""사용자 생성 성공 테스트"""
user = User(**sample_user)
assert user.username == "testuser"
assert user.email == "[email protected]"
assert user.age == 25
def test_user_creation_invalid_email(self):
"""잘못된 이메일로 사용자 생성 실패 테스트"""
with pytest.raises(ValidationError, match="Invalid email"):
User(username="test", email="invalid-email", age=25)
@pytest.mark.parametrize("age,should_raise", [
(-1, True), # 음수 나이
(0, False), # 0세 (허용)
(150, False), # 150세 (허용)
(151, True), # 151세 (불허용)
("25", True), # 문자열 (불허용)
])
def test_user_age_validation(self, age, should_raise):
"""나이 검증 테스트"""
if should_raise:
with pytest.raises(ValidationError):
User(username="test", email="[email protected]", age=age)
else:
user = User(username="test", email="[email protected]", age=age)
assert user.age == age
# tests/test_services.py - 서비스 테스트
import pytest
from unittest.mock import Mock, patch
from myapp.services import UserService, EmailService
from myapp.exceptions import UserNotFoundError, DuplicateUserError
class TestUserService:
"""사용자 서비스 테스트"""
def test_create_user_success(self, user_service, sample_user):
"""사용자 생성 성공 테스트"""
user_id = user_service.create_user(sample_user)
assert user_id is not None
created_user = user_service.get_user(user_id)
assert created_user.username == sample_user["username"]
def test_create_duplicate_user(self, user_service, sample_user):
"""중복 사용자 생성 실패 테스트"""
user_service.create_user(sample_user)
with pytest.raises(DuplicateUserError):
user_service.create_user(sample_user)
def test_get_nonexistent_user(self, user_service):
"""존재하지 않는 사용자 조회 테스트"""
with pytest.raises(UserNotFoundError):
user_service.get_user("nonexistent_id")
@patch('myapp.services.EmailService.send_welcome_email')
def test_create_user_sends_welcome_email(self, mock_send_email, user_service, sample_user):
"""사용자 생성 시 환영 이메일 발송 테스트"""
user_id = user_service.create_user(sample_user)
mock_send_email.assert_called_once()
call_args = mock_send_email.call_args[1]
assert call_args['email'] == sample_user['email']
# tests/test_integration.py - 통합 테스트
import pytest
import requests
from myapp.app import create_app
@pytest.fixture
def client():
"""테스트 클라이언트"""
app = create_app(testing=True)
with app.test_client() as client:
yield client
@pytest.mark.integration
class TestUserAPI:
"""사용자 API 통합 테스트"""
def test_create_user_endpoint(self, client, sample_user):
"""사용자 생성 엔드포인트 테스트"""
response = client.post('/api/users', json=sample_user)
assert response.status_code == 201
data = response.get_json()
assert 'id' in data
assert data['username'] == sample_user['username']
def test_get_user_endpoint(self, client, sample_user):
"""사용자 조회 엔드포인트 테스트"""
# 사용자 생성
create_response = client.post('/api/users', json=sample_user)
user_id = create_response.get_json()['id']
# 사용자 조회
get_response = client.get(f'/api/users/{user_id}')
assert get_response.status_code == 200
data = get_response.get_json()
assert data['username'] == sample_user['username']
def test_invalid_user_data(self, client):
"""잘못된 사용자 데이터 테스트"""
invalid_data = {
"username": "", # 빈 사용자명
"email": "invalid-email", # 잘못된 이메일
"age": -1 # 잘못된 나이
}
response = client.post('/api/users', json=invalid_data)
assert response.status_code == 400
errors = response.get_json()['errors']
assert 'username' in errors
assert 'email' in errors
assert 'age' in errors
# tests/factories.py - 테스트 데이터 팩토리
import factory
from faker import Faker
from myapp.models import User, Post, Comment
fake = Faker('ko_KR') # 한국어 로케일
class UserFactory(factory.Factory):
"""사용자 팩토리"""
class Meta:
model = User
username = factory.LazyAttribute(lambda obj: fake.user_name())
email = factory.LazyAttribute(lambda obj: fake.email())
first_name = factory.LazyAttribute(lambda obj: fake.first_name())
last_name = factory.LazyAttribute(lambda obj: fake.last_name())
age = factory.LazyAttribute(lambda obj: fake.random_int(min=18, max=80))
is_active = True
created_at = factory.LazyAttribute(lambda obj: fake.date_time_this_year())
class PostFactory(factory.Factory):
"""게시글 팩토리"""
class Meta:
model = Post
title = factory.LazyAttribute(lambda obj: fake.sentence(nb_words=6))
content = factory.LazyAttribute(lambda obj: fake.text(max_nb_chars=500))
author = factory.SubFactory(UserFactory)
created_at = factory.LazyAttribute(lambda obj: fake.date_time_this_year())
is_published = True
class CommentFactory(factory.Factory):
"""댓글 팩토리"""
class Meta:
model = Comment
content = factory.LazyAttribute(lambda obj: fake.text(max_nb_chars=200))
author = factory.SubFactory(UserFactory)
post = factory.SubFactory(PostFactory)
created_at = factory.LazyAttribute(lambda obj: fake.date_time_this_year())
# 팩토리 사용 예시
def test_with_factories():
"""팩토리를 사용한 테스트"""
# 단일 사용자 생성
user = UserFactory()
assert user.username is not None
assert '@' in user.email
# 여러 사용자 생성
users = UserFactory.create_batch(5)
assert len(users) == 5
# 특정 속성으로 생성
admin_user = UserFactory(username='admin', is_active=True)
assert admin_user.username == 'admin'
# 관련 객체와 함께 생성
post_with_comments = PostFactory()
comments = CommentFactory.create_batch(3, post=post_with_comments)
assert len(comments) == 3
assert all(comment.post == post_with_comments for comment in comments)
# tests/fixtures.py - 복잡한 픽스처들
import pytest
from tests.factories import UserFactory, PostFactory
@pytest.fixture
def users():
"""여러 사용자 픽스처"""
return {
'admin': UserFactory(username='admin', is_staff=True),
'user1': UserFactory(username='user1'),
'user2': UserFactory(username='user2'),
'inactive_user': UserFactory(username='inactive', is_active=False)
}
@pytest.fixture
def posts_with_comments(users):
"""댓글이 있는 게시글들"""
posts = []
for i in range(3):
post = PostFactory(author=users['user1'])
# 각 게시글에 랜덤한 수의 댓글 추가
CommentFactory.create_batch(
fake.random_int(min=1, max=5),
post=post,
author=users['user2']
)
posts.append(post)
return posts
@pytest.fixture
def api_headers():
"""API 테스트용 헤더"""
return {
'Content-Type': 'application/json',
'Accept': 'application/json'
}
@pytest.fixture
def authenticated_user(client, users):
"""인증된 사용자"""
user = users['user1']
# 로그인 시뮬레이션
login_data = {
'username': user.username,
'password': 'testpassword'
}
response = client.post('/auth/login', json=login_data)
token = response.get_json()['token']
return {
'user': user,
'token': token,
'headers': {'Authorization': f'Bearer {token}'}
}
# 테스트 성능 최적화 기법
# 1. 적절한 픽스처 스코프 사용
@pytest.fixture(scope="session") # 전체 세션에서 한 번만
def expensive_resource():
"""비용이 많이 드는 리소스"""
# 데이터베이스 연결, 외부 서비스 설정 등
resource = create_expensive_resource()
yield resource
resource.cleanup()
@pytest.fixture(scope="module") # 모듈당 한 번
def module_data():
"""모듈 레벨 데이터"""
return load_test_data()
@pytest.fixture(scope="function") # 각 테스트마다 (기본값)
def fresh_data():
"""매번 새로운 데이터"""
return create_fresh_data()
# 2. 병렬 테스트 실행
"""
pytest-xdist 사용:
pip install pytest-xdist
# 자동으로 CPU 코어 수만큼 병렬 실행
pytest -n auto
# 특정 개수의 프로세스로 실행
pytest -n 4
# 특정 테스트만 병렬 실행
pytest -n 2 tests/test_slow.py
"""
# 3. 느린 테스트 분리
@pytest.mark.slow
def test_heavy_computation():
"""무거운 연산 테스트"""
# 시간이 오래 걸리는 테스트
pass
@pytest.mark.integration
def test_external_api():
"""외부 API 테스트"""
# 외부 의존성이 있는 테스트
pass
# 실행 시 제외:
# pytest -m "not slow" # 느린 테스트 제외
# pytest -m "not integration" # 통합 테스트 제외
# 4. 메모리 효율적인 테스트
def test_large_dataset():
"""큰 데이터셋 테스트 (메모리 효율적)"""
# 생성기 사용으로 메모리 절약
def generate_test_data():
for i in range(10000):
yield {"id": i, "value": f"item_{i}"}
count = 0
for item in generate_test_data():
if item["id"] % 1000 == 0:
count += 1
assert count == 10
# 5. 테스트 캐싱
@pytest.fixture(scope="session")
def cached_api_data():
"""API 데이터 캐시"""
cache_file = "test_cache.json"
if os.path.exists(cache_file):
with open(cache_file) as f:
return json.load(f)
# 실제 API 호출 (비용이 큰 작업)
data = fetch_from_api()
with open(cache_file, 'w') as f:
json.dump(data, f)
return data
# 6. 조건부 테스트 실행
@pytest.mark.skipif(
not os.getenv("RUN_SLOW_TESTS"),
reason="느린 테스트는 환경변수가 설정된 경우만 실행"
)
def test_very_slow_operation():
"""매우 느린 연산 테스트"""
# RUN_SLOW_TESTS=1 pytest 로 실행
pass
# 7. 데이터베이스 테스트 최적화
@pytest.fixture(scope="module")
def db_with_data():
"""데이터가 미리 로드된 데이터베이스"""
db = create_test_db()
# 대량의 테스트 데이터를 한 번에 로드
bulk_insert_test_data(db)
yield db
db.close()
def test_query_performance(db_with_data):
"""쿼리 성능 테스트"""
start_time = time.time()
results = db_with_data.query("SELECT * FROM users WHERE age > 25")
end_time = time.time()
execution_time = end_time - start_time
assert len(results) > 0
assert execution_time < 0.1 # 100ms 이내
# 테스트 문서화 모범 사례
class TestUserRegistration:
"""
사용자 등록 기능 테스트
이 테스트 클래스는 사용자 등록 프로세스의 모든 측면을 검증합니다.
- 유효한 데이터로 등록 성공
- 잘못된 데이터로 등록 실패
- 중복 사용자 등록 방지
- 이메일 검증
"""
def test_successful_registration_with_valid_data(self, client):
"""
유효한 데이터로 사용자 등록 성공 테스트
Given: 유효한 사용자 데이터가 주어졌을 때
When: 등록 API를 호출하면
Then: 사용자가 성공적으로 생성되고 201 상태코드를 반환한다
"""
# Given
user_data = {
"username": "newuser",
"email": "[email protected]",
"password": "securepassword123"
}
# When
response = client.post('/api/register', json=user_data)
# Then
assert response.status_code == 201
data = response.get_json()
assert data['username'] == user_data['username']
assert data['email'] == user_data['email']
assert 'password' not in data # 비밀번호는 응답에 포함되지 않아야 함
def test_registration_fails_with_invalid_email(self, client):
"""
잘못된 이메일 형식으로 등록 실패 테스트
Given: 잘못된 이메일 형식의 사용자 데이터가 주어졌을 때
When: 등록 API를 호출하면
Then: 400 에러와 함께 이메일 검증 오류 메시지를 반환한다
"""
# Given
invalid_data = {
"username": "testuser",
"email": "invalid-email-format",
"password": "password123"
}
# When
response = client.post('/api/register', json=invalid_data)
# Then
assert response.status_code == 400
errors = response.get_json()['errors']
assert 'email' in errors
assert 'valid email' in errors['email'].lower()
@pytest.mark.parametrize("field,value,expected_error", [
("username", "", "username is required"),
("username", "a", "username must be at least 3 characters"),
("password", "123", "password must be at least 8 characters"),
("email", "", "email is required"),
])
def test_registration_validation_errors(self, client, field, value, expected_error):
"""
다양한 검증 오류 시나리오 테스트
Args:
field: 검증할 필드명
value: 잘못된 값
expected_error: 예상되는 오류 메시지
"""
# Given
base_data = {
"username": "validuser",
"email": "[email protected]",
"password": "validpassword123"
}
base_data[field] = value
# When
response = client.post('/api/register', json=base_data)
# Then
assert response.status_code == 400
errors = response.get_json()['errors']
assert field in errors
assert expected_error.lower() in errors[field].lower()
# 테스트 리포트 생성
"""
1. HTML 리포트 생성:
pip install pytest-html
pytest --html=reports/report.html --self-contained-html
2. JUnit XML 리포트 (CI/CD 통합용):
pytest --junitxml=reports/junit.xml
3. 커버리지 리포트:
pip install pytest-cov
pytest --cov=myapp --cov-report=html --cov-report=term
4. 종합 리포트:
pytest --html=reports/report.html --cov=myapp --cov-report=html --junitxml=reports/junit.xml
"""
# 커스텀 pytest 플러그인으로 리포팅 확장
# conftest.py
import pytest
import json
from datetime import datetime
def pytest_runtest_makereport(item, call):
"""테스트 실행 후 커스텀 리포트 생성"""
if call.when == "call":
# 테스트 결과 정보 수집
test_info = {
"name": item.name,
"nodeid": item.nodeid,
"outcome": call.excinfo is None,
"duration": call.duration,
"timestamp": datetime.now().isoformat()
}
# 커스텀 마크 정보 추가
marks = [mark.name for mark in item.iter_markers()]
test_info["marks"] = marks
# 결과를 파일에 저장 (실제로는 더 정교한 로깅 시스템 사용)
with open("test_results.jsonl", "a") as f:
f.write(json.dumps(test_info) + "\n")
# 테스트 메트릭 수집
class TestMetrics:
"""테스트 메트릭 수집 클래스"""
@staticmethod
def measure_performance(func):
"""성능 측정 데코레이터"""
def wrapper(*args, **kwargs):
start_time = time.time()
result = func(*args, **kwargs)
end_time = time.time()
# 성능 메트릭 로깅
print(f"함수 {func.__name__} 실행 시간: {end_time - start_time:.4f}초")
return result
return wrapper
# 테스트 태깅과 분류
"""
pytest.ini 설정으로 마크 정의:
[tool:pytest]
markers =
unit: 단위 테스트
integration: 통합 테스트
slow: 느린 테스트 (30초 이상)
fast: 빠른 테스트 (1초 이하)
smoke: 스모크 테스트 (기본 기능 확인)
regression: 회귀 테스트
api: API 테스트
database: 데이터베이스 테스트
external: 외부 의존성 테스트
critical: 중요한 테스트
"""
@pytest.mark.critical
@pytest.mark.api
def test_critical_api_endpoint():
"""중요한 API 엔드포인트 테스트"""
pass
@pytest.mark.fast
@pytest.mark.unit
def test_fast_unit_function():
"""빠른 단위 테스트"""
pass
# 테스트 실행 명령어 예시
"""
# 마크별 실행
pytest -m "unit and fast" # 빠른 단위 테스트만
pytest -m "not slow" # 느린 테스트 제외
pytest -m "critical or smoke" # 중요하거나 스모크 테스트
pytest -m "api and not external" # 외부 의존성 없는 API 테스트
# 커버리지와 함께
pytest -m "unit" --cov=myapp --cov-report=term-missing
# 병렬 실행과 함께
pytest -m "fast" -n auto
# 상세 리포트와 함께
pytest --html=report.html --cov=myapp --cov-report=html
"""
# 테스트 실행 스크립트 예시
# run_tests.py
import subprocess
import sys
import os
from pathlib import Path
def run_test_suite():
"""전체 테스트 스위트 실행"""
# 테스트 환경 설정
os.environ["TESTING"] = "true"
os.environ["DATABASE_URL"] = "sqlite:///:memory:"
# 리포트 디렉토리 생성
Path("reports").mkdir(exist_ok=True)
commands = [
# 1. 빠른 단위 테스트
[
"pytest", "-m", "fast and unit",
"--tb=short", "-v",
"--junitxml=reports/unit_tests.xml"
],
# 2. 통합 테스트
[
"pytest", "-m", "integration",
"--tb=short", "-v",
"--junitxml=reports/integration_tests.xml"
],
# 3. 전체 테스트 with 커버리지
[
"pytest",
"--cov=myapp",
"--cov-report=html:reports/coverage",
"--cov-report=term-missing",
"--html=reports/full_report.html",
"--self-contained-html",
"--junitxml=reports/all_tests.xml"
]
]
for i, cmd in enumerate(commands, 1):
print(f"\n{'='*50}")
print(f"실행 중: 테스트 단계 {i}/{len(commands)}")
print(f"명령어: {' '.join(cmd)}")
print(f"{'='*50}")
result = subprocess.run(cmd, capture_output=True, text=True)
if result.returncode != 0:
print(f"테스트 실패! 단계 {i}")
print("STDOUT:", result.stdout)
print("STDERR:", result.stderr)
return False
print(f"단계 {i} 완료 ✅")
print("\n🎉 모든 테스트 완료!")
print("📊 리포트 위치:")
print(" - HTML 리포트: reports/full_report.html")
print(" - 커버리지: reports/coverage/index.html")
print(" - JUnit XML: reports/")
return True
if __name__ == "__main__":
success = run_test_suite()
sys.exit(0 if success else 1)
# Makefile을 사용한 테스트 자동화
"""
# Makefile
.PHONY: test test-unit test-integration test-slow test-coverage test-all
# 기본 테스트
test:
pytest -v
# 단위 테스트만
test-unit:
pytest -m "unit" -v
# 통합 테스트만
test-integration:
pytest -m "integration" -v
# 느린 테스트
test-slow:
pytest -m "slow" -v
# 커버리지 테스트
test-coverage:
pytest --cov=myapp --cov-report=html --cov-report=term
# 전체 테스트 스위트
test-all:
pytest --cov=myapp --cov-report=html --html=reports/report.html
# CI용 테스트
test-ci:
pytest --cov=myapp --cov-report=xml --junitxml=junit.xml
# 테스트 환경 정리
clean-test:
rm -rf .pytest_cache
rm -rf htmlcov
rm -rf reports
rm -f .coverage
find . -type d -name __pycache__ -delete
"""
# GitHub Actions를 위한 워크플로우 예시
"""
# .github/workflows/test.yml
name: Tests
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: [3.8, 3.9, '3.10', '3.11']
steps:
- uses: actions/checkout@v3
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v3
with:
python-version: ${{ matrix.python-version }}
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt
pip install -r requirements-test.txt
- name: Run unit tests
run: |
pytest -m "unit" --junitxml=junit/unit-results.xml
- name: Run integration tests
run: |
pytest -m "integration" --junitxml=junit/integration-results.xml
- name: Run coverage
run: |
pytest --cov=myapp --cov-report=xml
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v3
with:
file: ./coverage.xml
- name: Upload test results
uses: actions/upload-artifact@v3
if: always()
with:
name: test-results-${{ matrix.python-version }}
path: junit/
"""
# 테스트 결과 분석 스크립트
# analyze_test_results.py
import json
import pandas as pd
import matplotlib.pyplot as plt
from pathlib import Path
def analyze_test_performance():
"""테스트 성능 분석"""
# 테스트 결과 로드
results = []
if Path("test_results.jsonl").exists():
with open("test_results.jsonl") as f:
for line in f:
results.append(json.loads(line))
if not results:
print("분석할 테스트 결과가 없습니다.")
return
df = pd.DataFrame(results)
# 기본 통계
print("=== 테스트 실행 통계 ===")
print(f"총 테스트 수: {len(df)}")
print(f"성공: {df['outcome'].sum()}")
print(f"실패: {len(df) - df['outcome'].sum()}")
print(f"평균 실행 시간: {df['duration'].mean():.4f}초")
print(f"최대 실행 시간: {df['duration'].max():.4f}초")
# 느린 테스트 식별
slow_tests = df[df['duration'] > df['duration'].quantile(0.9)]
print(f"\n=== 느린 테스트 (상위 10%) ===")
for _, test in slow_tests.iterrows():
print(f"{test['name']}: {test['duration']:.4f}초")
# 마크별 분석
if 'marks' in df.columns:
print("\n=== 마크별 통계 ===")
all_marks = []
for marks in df['marks']:
all_marks.extend(marks)
mark_counts = pd.Series(all_marks).value_counts()
print(mark_counts)
# 실행 시간 분포 그래프
plt.figure(figsize=(10, 6))
plt.hist(df['duration'], bins=30, alpha=0.7)
plt.xlabel('실행 시간 (초)')
plt.ylabel('테스트 수')
plt.title('테스트 실행 시간 분포')
plt.savefig('reports/test_duration_distribution.png')
print("\n📊 실행 시간 분포 그래프: reports/test_duration_distribution.png")
if __name__ == "__main__":
analyze_test_performance()
# 테스트 품질 체크리스트
"""
✅ 테스트 품질 체크리스트
1. 테스트 명명 규칙
- 테스트 함수명이 명확하고 설명적인가?
- 테스트가 무엇을 검증하는지 이름에서 알 수 있는가?
2. 테스트 구조 (AAA 패턴)
- Arrange (준비): 테스트 데이터와 환경 설정
- Act (실행): 테스트할 기능 실행
- Assert (검증): 결과 확인
3. 테스트 독립성
- 각 테스트가 독립적으로 실행 가능한가?
- 테스트 순서에 의존하지 않는가?
- 다른 테스트의 결과에 영향받지 않는가?
4. 테스트 범위
- 정상 케이스와 예외 케이스 모두 테스트하는가?
- 경계값 테스트가 포함되어 있는가?
- 코드 커버리지가 적절한가? (80% 이상 권장)
5. 테스트 성능
- 단위 테스트는 빠르게 실행되는가? (1초 이하)
- 느린 테스트는 적절히 분류되어 있는가?
- 외부 의존성을 모킹하고 있는가?
6. 테스트 유지보수성
- 테스트 코드가 읽기 쉽고 이해하기 쉬운가?
- 중복 코드가 픽스처나 헬퍼 함수로 추상화되어 있는가?
- 테스트 데이터가 적절히 관리되고 있는가?
7. 문서화
- 복잡한 테스트에 대한 설명이 있는가?
- 테스트의 목적과 컨텍스트가 명확한가?
8. CI/CD 통합
- 모든 테스트가 CI 파이프라인에서 실행되는가?
- 테스트 실패 시 적절한 알림이 설정되어 있는가?
- 테스트 결과가 적절히 리포팅되고 있는가?
"""