Develop/SW공학

테스트를 알아보자.

YOOZI. 2025. 4. 14. 16:13
728x90
품질을 위한 체계적인 검증 전략

 

 

 

오늘은 테스트에 대해 알아보자.

오늘의 배움
  • 소프트웨어 테스트의 기본 개념과 목적
  • 블랙박스/화이트박스 테스트 기법
  • 객체지향 테스트와 통합/시스템 테스트
  • 테스트 관리와 자동화 도구 (PyTest)

1.  소프트웨어 개발 테스트

 

  • 정의: 개발된 시스템이 올바르게 동작하는지 검증하는 과정으로, 버그를 발견하고 품질을 보장하는 활동
  • 한 줄 요약: "오류를 미리 발견하여 품질을 보장하고 유지보수성을 높이는 체계적인 검증 과정"
  • 특징:
    • 테스트는 '오류가 없다'를 증명하는 것이 아닌 '오류를 발견'하기 위한 과정
    • 다양한 테스트 기법과 레벨이 존재 (단위, 통합, 시스템 등)
    • 자동화 도구를 활용해 효율성 증대 가능
  • 필요성:
    • 결함 발견: 코드에 존재하는 오류를 조기에 발견하고 수정한다.
    • 소프트웨어 품질 보장: 예상된 동작과 실제 동작이 일치하는지 확인한다.
    • 개발 비용 절감: 개발 단계에서 버그를 발견하고 해결하면 비용을 절감할 수 있다.
    • 유지보수성 향상: 코드의 신뢰성을 높여 장기적인 유지보수성을 개선한다.
  • 장점/단점:
    • 장점: 품질 향상, 유지보수 비용 절감, 사용자 만족도 증가
    • 단점: 시간과 비용 소모, 완벽한 테스트는 불가능, 모든 결함을 찾을 수 없음
  • 예시: 은행 앱에서 송금 기능 테스트 - 올바른 계좌로 정확한 금액이, 중복 처리 없이, 보안적으로 안전하게 송금되는지 검증

 

쇼핑몰 주문 시스템 개발 예시:
  • 단위 테스트: 장바구니 클래스의 '상품 추가' 기능만 분리해서 테스트
  • 통합 테스트: 장바구니와 주문 처리 모듈이 함께 잘 작동하는지 테스트
  • 시스템 테스트: 사용자가 상품 선택부터 결제까지 전체 과정이 정상 작동하는지 테스트

2. 핵심 개념 정리

2-1. 블랙박스 테스트 (Black-Box Testing)

  • 정의: 내부 코드 구조를 고려하지 않고 입력과 출력만을 테스트하는 기법
  • 작동 원리: 명세를 기반으로 '무엇을 하는지'에 중점을 두고 기능적 측면 검증
  • 특징: 개발자가 아닌 테스터도 수행 가능, 사용자 관점에서 테스트
  • 장점/단점:
    • 장점: 개발자 편향 없음, 실제 사용 시나리오 기반 테스트 가능
    • 단점: 모든 경우의 수를 테스트하기 어려움, 내부 로직 누락 가능성
  • 필요성: 사용자 관점의 기능 검증과 요구사항 충족 여부 확인
  • 주요 기법:
    1. 동등 분할 테스트(ECP, Equivalence Class Partitioning): 입력값을 여러 개의 그룹으로 나누어 대표값을 테스트
    2. 경계값 분석(BVA, Boundary Value Analysis): 입력값의 경계 영역을 집중적으로 테스트
    3. 원인-결과 그래프(Cause-Effect Graphing): 입력 조건(원인)과 출력 동작(결과) 간의 관계를 그래프로 표현해 테스트 케이스 도출
    4. 결정 테이블 테스트(Decision Table Testing): 다양한 입력 조합에 대한 출력을 테스트하는 기법
    5. 상태 전이 테스트(State Transition Testing): 시스템의 상태 변화에 따른 동작을 검증
    6. 유스케이스 테스트(Use Case Testing): 실제 사용자 시나리오업무 흐름을 기반으로 기능 테스트
  • 예시:
# 블랙박스 테스트 예시 (경계값 분석)
def test_age_verification():
    # 경계값 분석: 18세 기준 성인 판별 함수
    assert is_adult(17) == False  # 경계값 아래
    assert is_adult(18) == True   # 경계값
    assert is_adult(19) == True   # 경계값 위

2-2. 화이트박스 테스트 (White-Box Testing)

  • 정의: 코드의 내부 구조와 로직을 분석하여 테스트 케이스를 설계하는 기법
  • 작동 원리: 'if-else', 반복문 등의 모든 논리적 경로를 확인
  • 특징: 개발자 수준의 지식 필요, 코드 커버리지 측정 가능
  • 장점/단점:
    • 장점: 코드 내부 흐름 검증, 논리적 오류 발견
    • 단점: 개발자 지식 필요, 시간 소모적, 요구사항 누락 발견 어려움
  • 필요성: 내부 구현의 정확성 보장, 모든 코드 경로 실행 확인
  • 주요 기법:
    1. 문장(구문) 커버리지(Statement Coverage) : 모든 개별 코드 라인(문장)이 최소 1회 이상 실행되었는지 확인
    2. 분기(결정) 커버리지(Decision / Branch Coverage): if, while, for 등의 분기점에서 True/False 경로를 각각 1회 이상 실행
    3. 조건 커버리지(Condition Coverage): 하나의 분기문 안에 있는 각 조건 요소(예: A && B)의 True/False 결과를 각각 1회 이상 테스트
    4. 다중 조건(복합 조건) 커버리지(Multiple Condition Coverage): 모든 조건 조합을 테스트 (예: A && B → TT, TF, FT, FF)
    5. 루프 테스트(Loop Testing): 반복문을 0회, 1회, 여러 회, 최대 회수 등 다양한 반복 조건에서 실행하며 검증
    6. 기본 경로 테스트(Basic Path Testing): 코드의 논리 복잡도(Cyclomatic Complexity)를 기반으로 독립적인 실행 경로를 모두 테스트
  • 예시:
# 화이트박스 테스트 예시 (분기 커버리지)
def discount_calculator(price, member_type):
    if member_type == "VIP":
        return price * 0.8  # 20% 할인
    elif member_type == "GOLD":
        return price * 0.9  # 10% 할인
    else:
        return price  # 할인 없음

def test_discount_calculator_branch_coverage():
    # 모든 분기 커버
    assert discount_calculator(1000, "VIP") == 800    # VIP 분기
    assert discount_calculator(1000, "GOLD") == 900   # GOLD 분기
    assert discount_calculator(1000, "REGULAR") == 1000  # 기본 분기

2-3. 객체지향 테스트

  • 정의: 객체 간의 상호작용을 고려하여 테스트하는 기법
  • 작동 원리: 캡슐화, 상속, 다형성 등 객체지향 특성을 고려한 테스트
  • 특징: 클래스 단위부터 시작해 점차 확장, 객체 간 메시지 전달 검증
  • 장점/단점:
    • 장점: 객체 간 상호작용 검증, 재사용성 높은 테스트 코드
    • 단점: 객체지향 설계 이해 필요, 복잡한 의존성 처리 필요
  • 필요성: 객체 기반 설계의 정확한 동작 보장
  • 주요 기법:
    1. 클래스 테스트: 단위 테스트 수준, 클래스/객체의 속성과 메서드 검증
    2. 통합 테스트: 여러 클래스를 통합하여 인터페이스/메시지 전달 검증
    3. 스레드 기반 테스트: 하나의 유스케이스를 구성하는 클래스 흐름 단위로 테스트
    4. 사용 기반 테스트: 상위 클래스 테스트 → 종속 클래스 테스트 흐름
    5. 확인 테스트: 요구사항 명세대로 구현되었는지 확인 (Validation)
    6. 시스템 테스트: 모든 컴포넌트가 통합되어 전체 동작을 테스트 (End-to-End)
  • 예시:
# 객체지향 테스트 예시 (클래스 테스트)
class BankAccount:
    def __init__(self, balance=0):
        self.balance = balance
        
    def deposit(self, amount):
        if amount > 0:
            self.balance += amount
            return True
        return False
    
    def withdraw(self, amount):
        if 0 < amount <= self.balance:
            self.balance -= amount
            return True
        return False

def test_bank_account():
    # 클래스 테스트
    account = BankAccount(1000)
    assert account.balance == 1000
    
    # 메서드 테스트
    assert account.deposit(500) == True
    assert account.balance == 1500
    
    assert account.withdraw(200) == True
    assert account.balance == 1300
    
    # 경계조건 테스트
    assert account.withdraw(0) == False  # 0원 출금 불가
    assert account.withdraw(1500) == False  # 잔액보다 큰 금액 출금 불가

2-4. 통합 테스트

  • 정의: 둘 이상의 모듈이나 클래스를 결합해 상호작용을 테스트하는 기법
  • 작동 원리: 컴포넌트 간 인터페이스와 데이터 흐름 검증
  • 특징: 여러 단위 테스트보다 복잡하고 환경 구성 필요
  • 장점/단점:
    • 장점: 객체 간 상호작용 확인, 단위 테스트로 찾기 어려운 오류 발견
    • 단점: 테스트 환경 구성 복잡, 실패 원인 파악 어려움
  • 필요성: 개별 컴포넌트가 함께 동작할 때 발생하는 문제 발견
  • 예시:
# 통합 테스트 예시
class ShoppingCart:
    def __init__(self):
        self.items = {}
    
    def add_item(self, product_id, quantity):
        if product_id in self.items:
            self.items[product_id] += quantity
        else:
            self.items[product_id] = quantity
    
    def get_total_quantity(self):
        return sum(self.items.values())

class OrderProcessor:
    def process_order(self, cart, payment_method):
        if not cart.items:
            return False
        
        # 실제로는 여기서 결제 처리
        return True

# 통합 테스트
def test_cart_and_order_integration():
    # 장바구니와 주문처리 객체 간 통합 테스트
    cart = ShoppingCart()
    processor = OrderProcessor()
    
    # 빈 장바구니로 주문 시도
    assert processor.process_order(cart, "credit") == False
    
    # 상품 추가 후 주문 처리
    cart.add_item("product1", 2)
    assert cart.get_total_quantity() == 2
    assert processor.process_order(cart, "credit") == True

2-5. 시스템 테스트

  • 정의: 모든 구성 요소를 통합한 전체 시스템의 동작을 검증하는 테스트
  • 작동 원리: 실제 사용 환경과 유사한 조건에서 엔드투엔드 시나리오 검증
  • 특징: 실제 사용자 관점, 기능 간 상호작용 확인
  • 장점/단점:
    • 장점: 전체 기능 흐름 확인, 실제 환경 유사한 테스트
    • 단점: 설정 복잡, 테스트 대상 크고 시간 소요 많음
  • 필요성: 개별 테스트로는 확인할 수 없는 시스템 전체의 품질 확인
  • 예시:
# 시스템 테스트 예시 (웹 애플리케이션)
from selenium import webdriver

def test_end_to_end_purchase():
    # 웹 브라우저 실행
    driver = webdriver.Chrome()
    try:
        # 쇼핑몰 접속
        driver.get("https://example-shop.com")
        
        # 로그인
        driver.find_element_by_id("username").send_keys("testuser")
        driver.find_element_by_id("password").send_keys("password123")
        driver.find_element_by_id("login-button").click()
        
        # 상품 검색 및 선택
        driver.find_element_by_id("search").send_keys("노트북")
        driver.find_element_by_id("search-button").click()
        driver.find_element_by_class_name("product-item").click()
        
        # 장바구니 추가
        driver.find_element_by_id("add-to-cart").click()
        
        # 결제 진행
        driver.find_element_by_id("checkout").click()
        
        # 주소 입력
        driver.find_element_by_id("address").send_keys("서울시 강남구")
        
        # 결제 방법 선택
        driver.find_element_by_id("payment-card").click()
        
        # 주문 완료
        driver.find_element_by_id("place-order").click()
        
        # 주문 성공 페이지 확인
        success_message = driver.find_element_by_id("order-confirmation").text
        assert "주문이 완료되었습니다" in success_message
        
    finally:
        driver.quit()

2-6. 테스트 자동화

  • 정의: 테스트 케이스를 자동으로 실행하고 결과를 확인하는 도구와 기법
  • 작동 원리: 테스트 스크립트를 작성하여 반복 실행, 결과 검증 자동화
  • 특징: 반복 가능, 일관성 있는 테스트, CI/CD 파이프라인 통합 가능
  • 장점/단점:
    • 장점: 시간 절약, 인적 오류 감소, 회귀 테스트 효율화
    • 단점: 초기 구축 비용, 유지보수 필요, 모든 테스트 자동화 불가
  • 필요성: 반복적인 테스트 효율화, 지속적 통합/배포(CI/CD) 지원
  • 자동화 도구:
    • JUnit (Java) : 단위 테스트, 자동화 테스트 지원
    • Jest (JavaScript) : 프론트엔드 및 백엔드 테스트 지원
    • PyTest (Python) : 단순한 API부터 복잡한 시스템 테스트까지 지원
      • 간결한 문법 + 강력한 기능 + 다양한 플러그인을 갖춘 Python 테스트 프레임워크로, 단위 테스트, 통합 테스트, UI 테스트(Selenium과 함께) 등 다양한 영역에서 사용된다.
    • Selenium (다양한 언어) : 웹 애플리케이션 UI 자동화 테스트
  • 예시 (PyTest):
# PyTest 자동화 테스트 예시
import pytest

# 테스트할 함수
def calculate_discount(price, quantity):
    if quantity >= 10:
        return price * 0.9  # 10개 이상 구매 시 10% 할인
    elif quantity >= 5:
        return price * 0.95  # 5개 이상 구매 시 5% 할인
    return price  # 할인 없음

# 파라미터화된 테스트
@pytest.mark.parametrize("price, quantity, expected", [
    (100, 1, 100),    # 할인 없음
    (100, 5, 95),     # 5% 할인
    (100, 10, 90),    # 10% 할인
    (100, 20, 90),    # 10% 할인 (최대)
])
def test_calculate_discount(price, quantity, expected):
    assert calculate_discount(price, quantity) == expected

# 예외 테스트
def divide(a, b):
    return a / b

def test_divide_by_zero():
    with pytest.raises(ZeroDivisionError):
        divide(5, 0)

# 픽스처 활용 예시
@pytest.fixture
def sample_db():
    # 테스트용 임시 DB 생성
    db = {"users": [{"id": 1, "name": "Test User"}]}
    yield db
    # 테스트 후 정리 작업

def test_with_db(sample_db):
    # 픽스처로 생성된 DB 사용
    assert len(sample_db["users"]) == 1
    assert sample_db["users"][0]["name"] == "Test User"

2-7. 실제 적용 예시

1. [온라인 쇼핑몰 결제 시스템 테스트]

온라인 쇼핑몰 결제 시스템을 테스트하는 경우:

 

블랙박스 테스트:

  • 동등 분할: 결제 금액을 범위별로 나누어 테스트 (1만원 미만, 1~10만원, 10만원 이상)
  • 경계값 분석: 무료배송 기준인 5만원 경계에서 49,999원, 50,000원, 50,001원 결제 테스트
  • 결정 테이블: 멤버십 등급(일반/VIP) × 결제 방법(카드/현금) × 할인쿠폰(유/무) 조합 테스트

화이트박스 테스트:

  • 분기 커버리지: 결제 프로세스의 모든 조건 분기(if-else) 테스트
  • 루프 테스트: 장바구니 상품 개수에 따른 반복 처리 검증

2. [PyTest를 활용한 사용자 인증 시스템 테스트]

# 사용자 인증 모듈
class UserAuth:
    def __init__(self):
        self.users = {"admin": "admin123", "user1": "pass123"}
    
    def authenticate(self, username, password):
        if username not in self.users:
            return "USER_NOT_FOUND"
        
        if self.users[username] != password:
            return "INVALID_PASSWORD"
        
        return "SUCCESS"

# PyTest로 테스트 자동화
import pytest

@pytest.fixture
def auth_system():
    return UserAuth()

def test_successful_login(auth_system):
    result = auth_system.authenticate("admin", "admin123")
    assert result == "SUCCESS"

def test_invalid_password(auth_system):
    result = auth_system.authenticate("admin", "wrong_pass")
    assert result == "INVALID_PASSWORD"

def test_user_not_found(auth_system):
    result = auth_system.authenticate("nonexistent", "anypass")
    assert result == "USER_NOT_FOUND"

# 파라미터화 테스트
@pytest.mark.parametrize("username, password, expected", [
    ("admin", "admin123", "SUCCESS"),
    ("admin", "wrong", "INVALID_PASSWORD"),
    ("unknown", "anypass", "USER_NOT_FOUND"),
    ("user1", "pass123", "SUCCESS"),
])
def test_authentication_scenarios(auth_system, username, password, expected):
    assert auth_system.authenticate(username, password) == expected

 

728x90