Python 고급 주제 (8‐9장): 예외처리와 테스트 - glasslego/getting-started-with-python GitHub Wiki

8. 예외처리

8.1 예외의 기본 개념

8.1.1 try-except 기본 구조

# 기본 예외 처리
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("오류가 발생했습니다.")  # 어떤 오류인지 알 수 없음

8.1.2 else와 finally 절

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문이 자동으로 파일을 닫아줌

8.2 주요 내장 예외들

8.2.1 일반적인 예외들

# 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}")

8.2.2 예외 계층 구조

# 예외 계층을 이용한 처리
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()

8.3 사용자 정의 예외

8.3.1 커스텀 예외 클래스

# 기본 커스텀 예외
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}")

8.3.2 예외 체이닝

# 예외 체이닝: 원래 예외를 유지하면서 새 예외 발생
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__

8.4 예외 처리 모범 사례

8.4.1 구체적인 예외 처리

# 나쁜 예: 너무 광범위한 예외 처리
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}' 파일 읽기 권한이 없습니다.")

8.4.2 예외 로깅과 디버깅

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()

8.4.3 컨텍스트 매니저와 예외 처리

# 커스텀 컨텍스트 매니저
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}")

9. 테스트

9.1 단위 테스트 (unittest)

9.1.1 기본 테스트 작성

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)

9.1.2 고급 테스트 기법

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())

9.2 pytest를 사용한 테스트

9.2.1 pytest 기본 사용법

# 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 설치 필요)

9.2.2 pytest 고급 기능

# 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
"""

9.3 테스트 모범 사례

9.3.1 테스트 구조와 조직

# 좋은 테스트 구조 예시

# 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

9.3.2 테스트 데이터 관리

# 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}'}
    }

9.3.3 테스트 성능과 최적화

# 테스트 성능 최적화 기법

# 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 이내

9.3.4 테스트 문서화와 리포팅

# 테스트 문서화 모범 사례

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 파이프라인에서 실행되는가?
   - 테스트 실패 시 적절한 알림이 설정되어 있는가?
   - 테스트 결과가 적절히 리포팅되고 있는가?
"""
⚠️ **GitHub.com Fallback** ⚠️