상태 전이 테스트로 시퀀스 결함을 검증하는 방법

상태 전이 테스트의 모델링, 커버리지, 가드와 액션 검증 방법을 정리하고 인증·결제·IoT 상태 흐름 설계에 적용한다.

2026-08-14 · 최초 발행 2025-12-17

상태 변화 자체를 테스트 대상으로 삼기

상태 전이 테스트(State Transition Testing)는 시스템이 어떤 상태에 있고, 이벤트를 받은 뒤 어디로 이동하는지를 테스트 케이스의 중심에 둔다. 상태 다이어그램(State Diagram)이나 유한 상태 기계(FSM)를 사용해 경로, 전이, 가드 조건, 액션을 모델링하고, 시퀀스·타이밍·누적 조건에서 생기는 결함을 검증한다.

인증과 세션, 워크플로우, 장치 라이프사이클, 결제 상태, 네트워크 프로토콜처럼 이전 상태가 다음 동작을 결정하는 로직에 적합하다. 검증 범위는 상태 커버리지, 전이 커버리지, 전이-쌍(1-switch) 커버리지, N-switch 커버리지, 액션·가드 커버리지로 나눌 수 있다.

모델에 모호함을 남기지 않는 방법

모델은 상태, 이벤트, 전이, 가드, 액션으로 구성한다. 각 요소를 정규화해 해석의 여지를 줄이고, 다중 가드가 있을 때는 우선순위와 우선 일치 전이를 분명히 정해야 한다. 예외나 무효 전이가 들어왔을 때의 처리 규칙도 모델에 포함한다.

테스트 강도는 상태·전이·N-switch 커버리지를 조합해 조절한다. 타임아웃, 리트라이, 장애 복구 같은 비기능 시나리오까지 다루려면 타임드 전이도 고려해야 한다.

실무 산출물은 상태 다이어그램, 전이 테이블, 테스트 경로 목록, 오라클 명세, 커버리지 리포트가 된다. MBT(model-based testing) 도구나 DSL, DFS/BFS/최단경로 기반 그래프 탐색기를 이용하면 경로 생성을 자동화할 수 있다.

모델 주도 접근은 시퀀스와 상태 의존 결함을 찾는 데 강하고 리그레션 자동화에도 유리하다. 대신 초기 모델링 비용과 복잡도가 늘며 상태 폭발 문제가 생길 수 있다. 계층화, 추상화, 표현 축소가 필요한 이유다.

요구사항에서 실행 가능한 경로까지

요구사항, 시퀀스 다이어그램, 상태 다이어그램 또는 상태를 생성할 텍스트 규칙에서 모델을 추출한다. 이후 전이 테이블을 만들고 N-switch 같은 커버리지 목표를 정한 뒤, 테스트 경로를 생성·축소하고 기대 상태와 액션을 오라클로 정의한다.

결과물에는 시퀀스 기반 테스트 케이스, 기대 상태와 액션, 무효 전이 목록, 커버리지 보고서가 포함된다. 무효 전이와 가드 경계값은 네거티브 케이스로 반드시 다뤄야 한다.

무효 전이가 발생하면 예외를 기대할지, 원래 상태를 유지할지, 롤백할지를 명시한다. 동시 이벤트에는 우선순위·락·재시도 정책이 필요하며, 타임아웃은 별도 타이머 상태 또는 타임드 가드로 표현할 수 있다.

단일 전이는 원자적인 상태 변화 단위로 정의하고 실패 시 보상 전이를 적용한다. 다중 리소스가 관련되면 2단계 커밋 또는 사가 패턴을 전이 체계에 반영한다. 상태와 액션이 맞지 않으면 즉시 불변조건(인바리언트)을 검증하고 롤백을 실행한다.

모델·경로·오라클이 연결되는 흐름

흐름흐름흐름전이 커버리지N-스위치 커버리지흐름흐름결과 수집아니오충분부족요구사항/상태 다이어그램모델 추출 정규화상태/이벤트/전이/가드/액션도출커버리지 기준 선택테스트 케이스 생성오라클(기대 상태/액션) 생성자동/수동 실행실제 상태 == 기대 상태커버리지 측정 분석결함 리포트 재현 절차종료

상태가 누적되는 도메인에서의 적용

계정 잠금 정책에서는 실패 횟수 누적에 따른 잠금 전이와 타임아웃·관리자 해제 전이를 모델에 넣는다. 잠긴 상태에서 로그인하려는 시도가 예외로 처리되는지도 네거티브 케이스로 검증한다.

결제 상태 머신은 Created→Authorized→Captured/Voided/Refunded 전이를 모델링할 수 있다. 금액 합계 일치, 중복 캡처 금지, 재시도 백오프 가드가 불변조건과 가드의 대상이 된다.

API 레이트리밋과 토큰 버킷은 토큰 소모·보충·차단 상태의 전이로 표현한다. 버킷 임계치±1과 동시 호출 충돌 시 락·재시도 가드를 경계 조건으로 확인한다.

IoT 디바이스는 Provisioned→Online→Degraded→Offline 흐름을 갖는다. 펌웨어 업데이트 도중 장애가 생겼을 때 보상 전이와 재개 정책이 기대대로 작동하는지 검증한다.

커버리지와 유지보수 관점의 차이

테스트 기법 성능(케이스 생성/실행 효율) 확장성(상태 수 증가 대응) 일관성(사양-테스트 정합) 안정성(변경 내성) 운영 편의(유지보수)
상태 전이 테스트 높음(모델 자동화 시) 중간~높음(추상화 전제) 높음 중간 중간
결정 테이블 테스트 중간 중간 중간 중간 높음
경계값 분석 높음 높음 낮음(상태 고려 부족) 높음 높음

전이 커버리지를 90% 이상 달성하면 세션·락·리트라이처럼 상태에 의존하는 결함의 검출률이 2040%p 향상되는 사례를 기대할 수 있다. 자동 경로 생성을 도입하면 테스트 설계 시간은 3060% 절감 가능하다.

요구사항과 설계 사이의 상태 불일치를 일찍 발견할 수 있고, 회귀 테스트의 재현성과 일관성도 높아진다. 변경이 미치는 범위를 더 명확히 파악할 수 있다는 점도 운영상 이점이다.

설계 시 확인할 항목

  • 모델에서는 상태를 병합·추상화해 최소화하고, 가드 조건과 무효 전이를 명확히 정의한다.
  • 상태·전이는 기본으로 커버하고 핵심 경로에는 1-switch 이상을 적용한다.
  • 동일 오라클·동일 액션을 갖는 경로는 병합하며, 루프와 사이클에는 상한을 둔다.
  • 가드 경계값을 데이터에 포함하고, 타임드 이벤트는 모의 타이머로 재현 가능하게 만든다.
  • 모델→케이스→오라클 생성 파이프라인을 스크립트화하고 커버리지 리포트와 연결한다.

Pytest로 검증하는 인증 상태 머신

전제조건

  • 환경: Python 3.11+, pytest >= 8.2
  • 설치: pip install pytest
  • 실행: pytest -q
# file: test_auth_fsm.py

class AuthFSM:
    def __init__(self, lock_threshold=3):
        self.state = "IDLE"
        self.fail_count = 0
        self.lock_threshold = lock_threshold

    def on_event(self, event: str):
        if self.state == "IDLE":
            if event == "login_success":
                self.fail_count = 0
                self.state = "AUTHENTICATED"
            elif event == "login_fail":
                self.fail_count += 1
                if self.fail_count >= self.lock_threshold:
                    self.state = "LOCKED"
            elif event == "timeout":
                pass  # stay IDLE
            else:
                raise ValueError("invalid transition from IDLE")
        elif self.state == "AUTHENTICATED":
            if event in ("logout", "timeout"):
                self.state = "IDLE"
            elif event in ("login_success", "login_fail"):
                raise ValueError("invalid transition from AUTHENTICATED")
            else:
                raise ValueError("invalid transition from AUTHENTICATED")
        elif self.state == "LOCKED":
            if event == "admin_reset":
                self.fail_count = 0
                self.state = "IDLE"
            elif event == "timeout":
                pass  # remain LOCKED
            else:
                raise ValueError("invalid transition from LOCKED")
        else:
            raise RuntimeError("unknown state")

import pytest

def test_transition_sequence_basic():
    f = AuthFSM(lock_threshold=2)
    assert f.state == "IDLE"
    f.on_event("login_fail");  assert f.state == "IDLE"
    f.on_event("login_fail");  assert f.state == "LOCKED"
    with pytest.raises(ValueError):
        f.on_event("login_success")  # invalid while locked
    f.on_event("admin_reset"); assert f.state == "IDLE"
    f.on_event("login_success"); assert f.state == "AUTHENTICATED"
    f.on_event("timeout"); assert f.state == "IDLE"

@pytest.mark.parametrize(
    "start,event",
    [
        ("IDLE", "logout"),
        ("AUTHENTICATED", "login_success"),
        ("LOCKED", "logout"),
    ],
)
def test_invalid_transitions(start, event):
    f = AuthFSM(lock_threshold=2)
    f.state = start  # test 전용 강제 세팅
    with pytest.raises(ValueError):
        f.on_event(event)

def test_pairwise_transition_1switch():
    # IDLE->AUTHENTICATED->IDLE 경로(1-switch) 검증
    f = AuthFSM()
    f.on_event("login_success")
    assert f.state == "AUTHENTICATED"
    f.on_event("logout")
    assert f.state == "IDLE"

이 예시는 유효 전이와 무효 전이를 모두 케이스로 만들고, 1-switch(전이-쌍) 커버리지로 연속 전이의 상호작용을 확인한다. 오라클은 다음 상태뿐 아니라 카운터 리셋 같은 부수 효과까지 검증해야 한다.

핵심 도메인 흐름에는 1-switch 이상을 적용하고, 나머지 경로는 전이 커버리지로 최소 보장할 수 있다. 자동 경로 생성과 오라클 스크립트화를 결합하면 효율을 높일 수 있으며, 상태 폭발은 추상화·계층 상태·가드 통합·경로 축소 휴리스틱으로 다룬다.

상태 전이 테스트모델 기반 테스트소프트웨어 테스트유한 상태 기계테스트 자동화