본문으로 건너뛰기
고급약 45분

MBT 도구 실습 — GraphWalker 로 모델에서 경로 뽑기

모델 파일을 JSON 으로 쓰고 검사한 뒤, 생성기와 정지 조건을 지정해 경로를 뽑고 실행 코드에 잇는 전 과정을 GraphWalker 로 돌려 본다. 공식 문서로 확인한 문법과 플래그만 쓰고, 확인되지 않은 상용 도구의 라이선스 상태는 단정하지 않는다.

  • #GraphWalker
  • #MBT도구
  • #정지조건
  • #모델파일
  • #테스트자동화

개념

이 레슨이 확인한 범위

도구 실습 레슨은 문법을 틀리면 못 쓰는 글이 된다. 그래서 출처를 먼저 밝힌다.

내용확인한 문서
생성기·정지 조건 목록과 조합 문법GraphWalker 프로젝트 공식 위키 Generators-and-stop-conditions
offline 하위 명령의 플래그·출력 형식같은 위키 Offline 페이지
JSON 모델 파일의 필드 이름graphwalker.github.io 의 JSON 모델 형식 레퍼런스
하위 명령 목록(offline·online·check·methods·requirements·convert·source)같은 사이트의 CLI 레퍼런스
Java 프로젝트의 애노테이션 기반 실행기 구조확인 못 함 — 원문 확인
TESTONA·Tosca 의 현재 라이선스·가격확인 못 함 — 원문 확인

GraphWalker 에는 원래의 Java 구현(graphwalker-cli-4.x.jar)과 이후의 Rust 구현(graphwalker-rs)이 있다. 아래 문법은 두 구현의 문서에서 같은 형태로 나타나는 부분만 썼지만, 쓰려는 배포본의 --help 출력을 확인하는 것이 가장 확실하다.

1단계 — 모델 파일을 JSON 으로 쓴다

레슨 2의 충전기 모델을 GraphWalker JSON 형식으로 옮긴다. 루트의 models 배열 안에서 각 모델이 vertices 와 edges 를 갖는다.

{
  "models": [
    {
      "name": "ChargerSession",
      "id": "charger-session",
      "generator": "quick_random(edge_coverage(100))",
      "startElementId": "e_start",
      "actions": ["kWh = 0;"],
      "vertices": [
        { "id": "v_idle", "name": "v_Idle" },
        { "id": "v_auth", "name": "v_Authenticating" },
        { "id": "v_charge", "name": "v_Charging" },
        { "id": "v_paused", "name": "v_Paused" },
        { "id": "v_ended", "name": "v_Ended" }
      ],
      "edges": [
        { "id": "e_start", "name": "e_Boot", "targetVertexId": "v_idle" },
        { "id": "e_tag", "name": "e_TagCard",
          "sourceVertexId": "v_idle", "targetVertexId": "v_auth" },
        { "id": "e_ok", "name": "e_AuthOk",
          "sourceVertexId": "v_auth", "targetVertexId": "v_charge",
          "actions": ["kWh = 0;"] },
        { "id": "e_ng", "name": "e_AuthFail",
          "sourceVertexId": "v_auth", "targetVertexId": "v_idle" },
        { "id": "e_pause", "name": "e_Pause",
          "sourceVertexId": "v_charge", "targetVertexId": "v_paused",
          "actions": ["kWh = kWh + 3;"] },
        { "id": "e_resume", "name": "e_Resume",
          "sourceVertexId": "v_paused", "targetVertexId": "v_charge",
          "guard": "kWh < 50" },
        { "id": "e_stop", "name": "e_StopRequest",
          "sourceVertexId": "v_charge", "targetVertexId": "v_ended" },
        { "id": "e_force", "name": "e_ForceStop",
          "sourceVertexId": "v_paused", "targetVertexId": "v_ended" },
        { "id": "e_settle", "name": "e_Settled",
          "sourceVertexId": "v_ended", "targetVertexId": "v_idle" }
      ]
    }
  ]
}

읽을 때 중요한 네 가지.

  • 시작 간선 — e_start 에는 sourceVertexId 가 없다. 출발 정점이 없는 간선이 시작 간선이 되고 startElementId 가 그것을 가리킨다.
  • guard — 불린 식이다. 위에서는 kWh < 50 일 때만 재개할 수 있다.
  • actions — 모델 변수를 갱신하는 스크립트다. 모델 수준은 초기화에 쓰고, 간선의 것은 그 전이를 탈 때 실행된다.
  • name 이 곧 메서드 이름 — v_Charging·e_Pause 가 실행 코드의 함수 이름이 된다. 그래서 이름에 공백이나 한글을 넣지 않는다.

GraphML 로도 같은 모델을 쓸 수 있다(yEd 같은 편집기로 그려 저장). 그림으로 리뷰해야 하면 GraphML, 코드 리뷰에서 diff 를 봐야 하면 JSON 이 낫다. CLI 레퍼런스에는 convert 로 GraphML 을 JSON 으로 바꿀 수 있다고 적혀 있다.

2단계 — 경로를 뽑기 전에 모델을 검사한다

레슨 1의 모델 린트를 도구가 일부 대신해 준다.

# 모델 파일 검증과 통계(간선·정점 수, 모델 문제)
graphwalker check -m model.json
 
# 모델에 등장하는 메서드 이름(정점·간선 이름) 목록을 중복 없이 정렬해 출력
graphwalker methods -m model.json
 
# 모델에 붙은 요구사항 키 목록
graphwalker requirements -m model.json

methods 의 출력이 그대로 구현해야 할 함수 목록 이다. 모델을 고친 뒤 이 명령을 다시 돌려 새로 생긴 이름이 있는지 보는 것이 유지보수 루틴의 첫 단계다(레슨 4).

requirements 는 추적성에 쓴다. 정점·간선의 requirements 필드에 요구사항 키를 달아 두면 정지 조건 requirement_coverage(100) 으로 "모든 요구사항을 최소 한 번 지나는 경로" 를 요구할 수 있다.

3단계 — 생성기와 정지 조건으로 경로를 뽑는다

offline 은 실행 없이 경로만 표준 출력으로 내보낸다. Java CLI 의 형태다.

java -jar graphwalker-cli-4.3.1.jar offline \
  -m model.json "quick_random(edge_coverage(100))"

출력은 한 줄에 하나씩 JSON 객체로 나온다.

{"currentElementName":"e_Boot"}
{"currentElementName":"v_Idle"}
{"currentElementName":"e_TagCard"}
{"currentElementName":"v_Authenticating"}
{"currentElementName":"e_AuthOk"}
{"currentElementName":"v_Charging"}
...

이름만 뽑아 쓰려면 jq 를 쓴다.

java -jar graphwalker-cli-4.3.1.jar offline \
  -m model.json "quick_random(edge_coverage(100))" \
  | jq -r '.currentElementName' > path.txt

offline 의 플래그 중 실무에서 쓰는 것은 네 개다.

플래그뜻
-m / --model모델 파일과 생성기·정지 조건. 반복 지정 가능(여러 모델 연결)
-s / --seed무작위 생성기의 시드 고정 → 재현 가능
-u / --unvisited남은 미방문 요소를 함께 출력 → 왜 100% 가 안 됐는지 진단
-o / --verbose상세 출력

--seed 가 레슨 2의 "무작위 순회의 재현" 문제를 해결하는 지점이다. 실패한 야간 실행의 시드를 로그에 남겨 두면 같은 경로를 다시 걸을 수 있다.

생성기와 정지 조건 문법

공식 위키에 열거된 생성기는 다음과 같다.

생성기성격
random(조건)완전 무작위 순회. 문서에 "Drunkard's walk" 로 설명돼 있다
weighted_random(조건)간선의 weight 를 확률로 사용
quick_random(조건)미방문 간선을 무작위로 고른 뒤 최단 경로로 이동
a_star(조건)특정 정점·간선까지의 최단 경로
predefined_path(조건)모델의 predefinedPathEdgeIds 순서를 그대로 실행
shortest_all_paths문서에 미출시 로 표기돼 있다

정지 조건은 다음과 같다.

정지 조건뜻
edge_coverage(N)간선 커버리지 N% 달성 시 정지. 같은 간선을 여러 번 지나도 1로 센다
vertex_coverage(N)정점 커버리지 N% 달성 시 정지
requirement_coverage(N)요구사항 커버리지 N% 달성 시 정지
dependency_edge_coverage(N)dependency 값이 임계치 이상인 간선을 덮으면 정지
reached_vertex(이름)지정한 정점에 도달하면 정지
reached_edge(이름)지정한 간선을 지나면 정지
time_duration(초)지정한 초가 지나면 정지
length(정수)지정한 개수의 요소를 생성하면 정지
never멈추지 않는다

조합은 or(또는 ||)와 and(또는 &&)로 하고 괄호로 묶는다. 공식 문서의 예시가 이런 형태다.

random(edge_coverage(100) || time_duration(500))
random(reached_vertex(v_SomeVertex) && edge_coverage(100))
a_star(reached_edge(e_SomeEdge))

정지 조건을 고르는 기준

never 와 time_duration 이 있다는 사실이 도구의 성격을 말해 준다. MBT 는 "케이스 N개를 만든다" 가 아니라 "조건을 만족할 때까지 걷는다" 다.

목적정지 조건
회귀 — 모든 전이를 한 번씩, 짧게quick_random(edge_coverage(100))
커버리지는 채우되 시간 상한이 있다`quick_random(edge_coverage(100)
야간 장시간 안정성 탐색random(time_duration(7200))
결함 재현 경로 1건a_star(reached_edge(e_ForceStop))
요구사항 추적성 증빙quick_random(requirement_coverage(100))

|| 조합이 실무에서 가장 중요하다. 커버리지만 조건으로 걸면 끝나지 않을 수 있다. 레슨 1의 도달 불가 상태가 하나라도 있으면 edge_coverage(100) 은 만족되지 않는다. 그래서 CI 설정에는 time_duration 이나 length 를 || 로 묶어 상한을 둔다. -u 로 미방문 요소를 찍으면 "무엇을 못 밟았는지" 가 로그에 남는다.

4단계 — 뽑은 경로를 실행 코드에 잇는다

두 가지 구조가 있고, 가드가 있는 모델이냐가 선택을 가른다.

오프라인 방식 — 경로를 먼저 파일로 뽑고, 그 파일을 읽어 이름에 해당하는 함수를 순서대로 호출한다. 이름 → 함수 매핑 한 장이 전부다.

# runner.py — path.txt 의 이름을 순서대로 실행한다
import json, sys
 
class ChargerAdapter:
    """정점 메서드 = 검증, 간선 메서드 = 동작"""
    def e_Boot(self):        self.api = Charger(); self.api.boot()
    def e_TagCard(self):     self.api.tag("CARD-0001")
    def e_AuthOk(self):      self.api.auth_result(ok=True)
    def e_AuthFail(self):    self.api.auth_result(ok=False)
    def e_Pause(self):       self.api.pause()
    def e_Resume(self):      self.api.resume()
    def e_StopRequest(self): self.api.stop()
    def e_ForceStop(self):   self.api.force_stop()
    def e_Settled(self):     self.api.settle()
 
    def v_Idle(self):        assert self.api.state() == "IDLE"
    def v_Authenticating(self): assert self.api.state() == "AUTH"
    def v_Paused(self):      assert self.api.state() == "PAUSED"
    def v_Charging(self):
        assert self.api.state() == "CHARGING"
        assert self.api.power_kw() > 0          # 상태 불변식
    def v_Ended(self):
        assert self.api.state() == "ENDED"
        assert self.api.receipt_no() is not None
 
adapter = ChargerAdapter()
for line in sys.stdin:
    name = json.loads(line)["currentElementName"]
    getattr(adapter, name)()     # methods 명령의 출력이 이 목록이다

graphwalker methods -m model.json 의 출력과 이 클래스의 메서드 목록이 정확히 일치해야 한다. 둘을 비교하는 테스트를 하나 두면 모델에 정점을 추가했는데 구현을 빠뜨린 상황이 즉시 실패로 드러난다 — 가장 값싼 안전망이다.

온라인 방식 — online 하위 명령으로 GraphWalker 를 REST 또는 WebSocket 서비스로 띄우고, 테스트 코드가 "다음에 뭘 할까" 를 매 단계 물어본다.

graphwalker online -s RESTFUL -p 8080 \
  -m model.json "quick_random(edge_coverage(100))"

가드가 있으면 온라인 방식이 필요하다. e_Resume 의 가드 kWh < 50 은 경로를 걷는 동안 간선 액션으로 바뀌는 값이다. 오프라인으로 미리 뽑은 경로는 생성 시점의 모델 변수만 반영한다. 가드가 없고 그래프 구조만 있는 모델이면 오프라인이 단순하고 CI 에 넣기 쉽다.

오프라인온라인
경로 생성 시점실행 전실행 중 매 단계
가드·모델 변수생성 시점 값만실행 중 평가
CI 연결쉽다. 경로 파일을 아티팩트로 보관서비스 기동이 필요
실패 재현경로 파일이 그대로 재현 자료시드와 로그를 함께 남겨야 한다

상용 도구 — 확인한 것만

도구성격라이선스·입수
GraphWalker오픈소스. 그래프 모델을 순회해 경로를 생성. Java·Rust 두 구현공개 저장소. 라이선스 조건은 저장소 원문 확인
TESTONA분류 트리 기법 설계 도구(모듈 3 레슨 6). 기능 조합 중심이며 상태 순회 도구와 성격이 다르다기능을 줄인 Light 판이 비상업용으로 배포돼 왔고 평가판이 안내된다. 현재 배포 주체·조건은 제품 페이지 원문 확인
Tricentis Tosca상용 스위트. 모델 기반 자동화를 제품 특징으로 내세운다상용. 가격·평가판 조건·메뉴 구성은 벤더 문서 원문 확인

상용 도구는 버전마다 메뉴 이름과 라이선스 체계가 바뀐다. 평가 자리에서는 세 가지만 확인하면 된다 — 모델을 파일로 내보낼 수 있는가(종속 탈출 가능성), 생성 기준을 지정할 수 있는가(레슨 2의 커버리지 기준), CI 에서 명령행으로 돌릴 수 있는가. 세 번째를 못 하는 도구는 파이프라인에 들어오지 못한다.

실무에서 왜 필요한가

MBT 는 "개념은 알겠는데 그래서 뭘 깔아서 어떻게 돌리나" 에서 멈추는 기법이다. JSON 열 줄짜리 모델과 명령 한 줄로 경로가 나오는 것을 한 번 보고 나면, 도입 논의가 "할 수 있나" 에서 "어디에 쓸까" 로 넘어간다.

실무 기여가 가장 큰 부분은 의외로 check·methods 다. 모델 파일이 저장소에 있고 두 명령이 CI 에서 돌아가면 모델 결함과 구현 누락이 코드 리뷰 단계에서 잡힌다. 레슨 1의 린트 항목을 사람이 눈으로 보는 것과, 모델을 수정한 커밋이 자동으로 걸리는 것은 운영 비용이 다르다.

정지 조건 설계도 그대로 산출물이 된다. edge_coverage(100) 만 걸어 파이프라인이 멈추지 않는 사고, length(30) 만 걸어 커버리지가 60% 에서 끝나는 사고가 둘 다 흔하다. || 로 상한을 묶고 -u 로 미방문을 찍는 설정 한 줄이 둘을 막는다.

실습 과제

과제 1 — 모델을 쓰고 검사까지 돌리기 (25분)

본문의 충전기 모델 JSON 을 그대로 파일로 만들고, 거기에 레슨 2 과제의 전이 C -> A (긴급정지)를 추가한다. 그 뒤 순서대로 확인한다.

  1. check 로 간선·정점 수가 예상과 맞는지 본다
  2. methods 출력을 저장한다 — 이것이 구현할 함수 목록이다
  3. 간선 하나의 targetVertexId 를 없는 id 로 바꿔 check 를 다시 돌리고, 어떤 메시지가 나오는지 기록한 뒤 되돌린다

3번이 핵심이다. 도구가 잡아 주는 모델 결함과 사람만 잡을 수 있는 모델 결함의 경계 를 확인하는 것이다. 레슨 1의 여섯 기준 중 어느 것이 check 로 잡히고 어느 것이 안 잡히는지 표로 적는다.

과제 2 — 같은 모델, 네 가지 설정 (25분)

같은 모델 파일로 아래 네 설정을 각각 offline 으로 돌려 결과를 비교한다.

A  quick_random(edge_coverage(100))
B  random(edge_coverage(100))
C  random(length(40))
D  a_star(reached_vertex(v_Ended))

각각에 대해 생성된 요소 수, 중복으로 지나간 간선 수, 끝에 미방문 요소가 남았는지 를 표로 적는다(-u 를 함께 쓴다). 그리고 B 를 --seed 를 바꿔 세 번, 같은 시드로 두 번 돌려 결과를 비교한다. A 와 B 의 길이 차이가 레슨 2의 "짧은 결정적 경로 대 긴 무작위 경로" 표에서 어디에 해당하는지 쓴다.

과제 3 — 어댑터를 붙여 끝까지 돌리기 (30분)

본문의 runner.py 구조를 그대로 써서, 실제 시스템 대신 상태를 들고 있는 가짜 구현(fake) 을 만들어 경로를 끝까지 실행한다.

  1. Charger 를 상태 문자열 하나와 receipt_no 만 가진 가짜로 만든다
  2. offline 출력을 파이프로 runner.py 에 넣어 통과하는 것을 확인한다
  3. 의도적인 결함 을 넣는다 — force_stop() 이 영수증 번호를 발급하지 않게 한다
  4. 다시 돌려 v_Ended 불변식에서 실패하는 것과, 그 시점까지의 경로가 로그에 남아 재현 가능한지 를 확인한다

4번이 MBT 운영의 실제 모습이다. "어떤 경로로 갔더니 실패했는가" 가 로그에 남지 않으면 그 실패는 티켓으로 옮길 수 없다.

자가 체크리스트

  • GraphWalker JSON 모델의 vertices·edges·guard·actions·startElementId 를 설명할 수 있다
  • 출발 정점이 없는 간선이 시작 간선이 된다는 것을 안다
  • 정점·간선의 name 이 실행 코드의 함수 이름과 같아야 하는 이유를 안다
  • check·methods·requirements 로 모델을 검사하고 구현 목록을 뽑을 수 있다
  • 생성기 여섯 개의 이름과 성격을 구분할 수 있다
  • 정지 조건을 목적에 맞게 고르고 || 로 상한을 묶을 수 있다
  • edge_coverage(100) 만 걸었을 때 끝나지 않을 수 있는 이유를 설명할 수 있다
  • --seed 와 -u 플래그를 언제 쓰는지 안다
  • 오프라인 방식과 온라인 방식을 구분하고, 가드가 있을 때 어느 쪽이 필요한지 안다
  • 상용 MBT 도구를 평가할 때 확인할 세 가지(모델 내보내기·기준 지정·명령행 실행)를 말할 수 있다

흔한 실수

  • 정점·간선 이름에 공백이나 한글을 넣는다. 그 이름이 함수 이름이 되므로 실행 코드에서 매핑이 깨진다. 영문과 밑줄만 쓴다.
  • edge_coverage(100) 만 정지 조건으로 걸고 CI 에 넣는다. 도달 불가 요소가 하나만 있어도 파이프라인이 끝나지 않는다. || 로 시간·길이 상한을 묶는다.
  • 무작위 생성의 시드를 남기지 않는다. 실패한 경로를 다시 만들 수 없다.
  • 가드가 있는 모델을 오프라인으로 뽑아 쓴다. 생성 시점의 모델 변수만 반영돼 실제 시스템 상태와 어긋난 경로가 나온다.
  • 모델과 구현의 이름 목록을 비교하지 않는다. 모델에 정점을 추가하고 구현을 빠뜨리면 실행 중에야 터진다. methods 출력과 대조하는 테스트를 하나 둔다.
  • 상용 도구의 메뉴 순서를 팀 위키에 상세히 적는다. 다음 버전에서 틀린 문서가 된다. 확인할 기준 을 적고 화면은 적지 않는다. 명령 예시의 jar 버전도 배포본마다 다르므로 --help 로 확인하는 절차를 함께 쓴다.

참고 자료

  • GraphWalker — Generators and stop conditions — 생성기·정지 조건의 정식 목록과 or/and 조합 문법
  • GraphWalker — Offline — offline 명령의 플래그와 currentElementName 출력 형식, jq 사용 예
  • GraphWalker — 문서 사이트 — JSON 모델 형식 레퍼런스와 CLI 하위 명령(check·methods·requirements·convert·source) 설명. 두 구현의 차이는 각 저장소 원문 확인
  • TESTONA · Tricentis Tosca — 판본·라이선스·가격은 각 제품 페이지 원문 확인
  • 모듈 11 레슨 6 — 자동화 프레임워크 유형. 이 레슨의 어댑터 계층이 들어가는 자리를 그 구조도에서 본다

낯선 말 풀이

이 레슨에 나온 약어입니다. 본문에서도 처음 나올 때 점선으로 표시해 두었습니다 (제목과 링크 글자는 그대로 둡니다).

JSON — 제이슨
프로그램끼리 값을 주고받을 때 쓰는 글자 형식. 괄호와 따옴표로 이름과 값을 짝지어 적는다.
CLI — 명령 줄 인터페이스
검은 화면에 글자로 명령을 적어 넣는 방식. 버튼은 없고 줄마다 명령을 쓴다.
MBT — 모델 기반 테스팅
화면 흐름이나 상태 변화를 그림(모델)으로 먼저 그리고, 거기서 시험할 경로를 기계적으로 뽑아내는 방식.
CI — 지속적 통합
코드를 올릴 때마다 자동으로 합쳐서 빌드하고 시험까지 돌리는 방식.
REST — 레스트
창구를 만드는 가장 흔한 방식. 주소 하나가 자원 하나를 가리키고, 동작은 정해진 몇 가지 말로 구분한다.

용어집 전체 보기