feat(tester): add client journey testing standard

This commit is contained in:
2026-10-15 16:48:54 +08:00
parent baed23d02a
commit ffa7cbd63e
12 changed files with 518 additions and 0 deletions
+104
View File
@@ -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()
+69
View File
@@ -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()