feat(tester): add client journey testing standard
This commit is contained in:
@@ -0,0 +1,104 @@
|
||||
#!/usr/bin/env python3
|
||||
import json
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
import yaml
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
CATALOG = ROOT / "scripts" / "catalog.py"
|
||||
|
||||
|
||||
def write_fixture(root: Path, *, complete: bool = True) -> None:
|
||||
tester = root / ".pouch" / "tester"
|
||||
(tester / "cases").mkdir(parents=True)
|
||||
(tester / "runs").mkdir()
|
||||
(tester / "catalog.yaml").write_text(
|
||||
yaml.safe_dump(
|
||||
{
|
||||
"version": 2,
|
||||
"id_pattern": "^TC-[A-Z]+-[0-9]+$",
|
||||
"categories": [{"id": "CLIENT"}],
|
||||
"risks": ["readonly", "mutating", "destructive"],
|
||||
"default_max_risk": "readonly",
|
||||
"client_suites": {
|
||||
"client-smoke": {"default": True},
|
||||
"client-regression": {"default": False},
|
||||
"safety": {"default": False},
|
||||
},
|
||||
"sessions": {"browser": {"kind": "browser", "reuse": "run"}},
|
||||
},
|
||||
sort_keys=False,
|
||||
),
|
||||
encoding="utf-8",
|
||||
)
|
||||
case = {
|
||||
"id": "TC-CLIENT-001",
|
||||
"category": "CLIENT",
|
||||
"title": "journey",
|
||||
"what": "client journey",
|
||||
"lifecycle": "active",
|
||||
"tag": "smoke",
|
||||
"risk": "readonly",
|
||||
"driver": "browser",
|
||||
"session": "browser",
|
||||
"client_suite": "client-smoke",
|
||||
"platform": ["android", "ios"],
|
||||
"journey": {
|
||||
"goal": "goal",
|
||||
"entry_state": "logged_out",
|
||||
"actions": ["start", "login"],
|
||||
"assertions": ["visible"],
|
||||
"final_state": "done",
|
||||
"failure_policy": "stop",
|
||||
},
|
||||
"fixture": {"account": "test-role"},
|
||||
"cleanup": {"steps": ["cleanup"]},
|
||||
"how": {"steps": ["start", "login"]},
|
||||
"expected": [{"kind": "visible-text", "value": "done"}],
|
||||
}
|
||||
if not complete:
|
||||
case.pop("fixture")
|
||||
case.pop("cleanup")
|
||||
(tester / "cases" / "client.yaml").write_text(
|
||||
yaml.safe_dump({"category": "CLIENT", "cases": [case]}, sort_keys=False),
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
|
||||
class CatalogJourneyTests(unittest.TestCase):
|
||||
def run_catalog(self, root: Path, *args: str):
|
||||
return subprocess.run(
|
||||
[sys.executable, str(CATALOG), *args, "--root", str(root)],
|
||||
text=True,
|
||||
capture_output=True,
|
||||
)
|
||||
|
||||
def test_valid_client_catalog_and_suite_filter(self):
|
||||
with tempfile.TemporaryDirectory() as temp:
|
||||
root = Path(temp)
|
||||
write_fixture(root)
|
||||
valid = self.run_catalog(root, "validate")
|
||||
self.assertEqual(valid.returncode, 0, valid.stderr)
|
||||
selected = self.run_catalog(root, "select", "--suite", "client-smoke")
|
||||
self.assertEqual(selected.returncode, 0, selected.stderr)
|
||||
self.assertEqual(json.loads(selected.stdout)["count"], 1)
|
||||
|
||||
def test_missing_fixture_or_cleanup_blocks_before_results(self):
|
||||
with tempfile.TemporaryDirectory() as temp:
|
||||
root = Path(temp)
|
||||
write_fixture(root, complete=False)
|
||||
selected = self.run_catalog(root, "select", "--suite", "client-smoke")
|
||||
self.assertNotEqual(selected.returncode, 0)
|
||||
payload = json.loads(selected.stdout)
|
||||
self.assertEqual(payload["preflight"]["failure_class"], "contract")
|
||||
self.assertTrue(any("fixture 缺失" in error for error in payload["preflight"]["errors"]))
|
||||
self.assertEqual(payload["preflight"]["status"], "blocked")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,69 @@
|
||||
#!/usr/bin/env python3
|
||||
import sys
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "scripts"))
|
||||
from journey import classify_outcome, contract_errors, propagate_results
|
||||
|
||||
|
||||
def valid_case(**overrides):
|
||||
case = {
|
||||
"id": "TC-CLIENT-001",
|
||||
"client_suite": "client-smoke",
|
||||
"driver": "browser",
|
||||
"session": "browser",
|
||||
"platform": ["android", "ios"],
|
||||
"journey": {
|
||||
"goal": "完成客户端目标",
|
||||
"entry_state": "logged_out",
|
||||
"actions": ["启动", "登录", "完成目标"],
|
||||
"assertions": ["看到目标页面"],
|
||||
"final_state": "目标已完成",
|
||||
"failure_policy": "stop",
|
||||
},
|
||||
"fixture": {"account": "test-role", "data": []},
|
||||
"cleanup": {"steps": ["清理本轮数据"]},
|
||||
}
|
||||
case.update(overrides)
|
||||
return case
|
||||
|
||||
|
||||
class JourneyContractTests(unittest.TestCase):
|
||||
def test_contract_accepts_shared_client_shape(self):
|
||||
self.assertEqual(contract_errors(valid_case(), {"browser": {}}), [])
|
||||
|
||||
def test_missing_fixture_and_cleanup_are_contract_errors(self):
|
||||
errors = contract_errors(
|
||||
valid_case(fixture=None, cleanup=None),
|
||||
{"browser": {}},
|
||||
)
|
||||
self.assertTrue(any("fixture 缺失" in error for error in errors))
|
||||
self.assertTrue(any("cleanup 缺失" in error for error in errors))
|
||||
|
||||
def test_login_failure_is_product_failure_but_unavailable_environment_is_not(self):
|
||||
self.assertEqual(classify_outcome(ok=False, phase="login"), "fail")
|
||||
self.assertEqual(
|
||||
classify_outcome(ok=False, phase="login", environment_ready=False),
|
||||
"environment-fail",
|
||||
)
|
||||
|
||||
def test_failed_case_blocks_remaining_cases_and_keeps_explicit_skip(self):
|
||||
rows = propagate_results(
|
||||
["TC-1", "TC-2", "TC-3"],
|
||||
{"TC-1": "fail", "TC-2": "pass", "TC-3": "pass"},
|
||||
skipped={"TC-3": "未授权"},
|
||||
)
|
||||
self.assertEqual([row["result"] for row in rows], ["fail", "not-run", "skipped"])
|
||||
|
||||
def test_continue_policy_allows_next_case(self):
|
||||
rows = propagate_results(
|
||||
["TC-1", "TC-2"],
|
||||
{"TC-1": "fail", "TC-2": "pass"},
|
||||
policies={"TC-1": "continue"},
|
||||
)
|
||||
self.assertEqual([row["result"] for row in rows], ["fail", "pass"])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user