개념
이 레슨이 확인한 범위
도구 실습 레슨은 문법을 틀리면 못 쓰는 글이 된다. 그래서 출처를 먼저 밝힌다.
| 내용 | 확인한 문서 |
|---|---|
| 생성기·정지 조건 목록과 조합 문법 | 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.jsonmethods 의 출력이 그대로 구현해야 할 함수 목록 이다. 모델을 고친 뒤 이
명령을 다시 돌려 새로 생긴 이름이 있는지 보는 것이 유지보수 루틴의 첫
단계다(레슨 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.txtoffline 의 플래그 중 실무에서 쓰는 것은 네 개다.
| 플래그 | 뜻 |
|---|---|
-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 로 미방문을 찍는 설정 한 줄이 둘을 막는다.