Files
CareerNexus/tests/test_match.py
T
RoyirinRosmontis a10442d78c CareerNexus 职联:岗位能力图谱构建与动态演化分析平台(挑战杯 XH-202621)
- 全链路:多源JD接入/清洗交叉验证/结构化抽取(星火+离线双通道)/时序知识图谱/新岗位发现/演化分析/人岗匹配/学习路径
- WebUI 五视图:驾驶舱/图谱工作台/岗位演化/匹配(单人+批量)/学习路径
- 设置中心(config.json)/Token鉴权/SQLite状态库/语义向量通道(BGE-M3本地或联网API)
- 71 个 pytest 用例;数据目录仅保留结构(.gitkeep),数据由 scripts/generate_dataset.py 复现
2026-08-03 13:15:35 +08:00

94 lines
4.3 KiB
Python

"""匹配引擎测试:评分权重、差距清单、可迁移技能、学习路径拓扑序、简历脱敏。"""
from __future__ import annotations
from app.extract.base import load_lexicon
from app.match.engine import MatchEngine, topological_stages
from app.match.resume import mask_text, parse_resume
from app.models import JobDefinition, ResumeProfile
JOB = JobDefinition(
job_id="job_X", title="X工程师", category="后端开发",
required_skills=["Java", "Spring Boot", "MySQL", "Redis"],
preferred_skills=["Docker", "Kubernetes"],
scenarios=["电商", "金融"], expected_years=3,
)
def _engine(**weights) -> MatchEngine:
return MatchEngine(lexicon=load_lexicon(), weights=weights or None)
def test_scoring_weights_and_dimensions():
"""四维加权:必备50/加分15/经验20/场景15。"""
profile = ResumeProfile(
skills=["Java", "Spring Boot", "Docker"], # 必备 2/4,加分 1/2
years=1.5, # 经验 1.5/3 = 0.5
scenarios=["电商"], # 场景 1/2
)
r = _engine().match(profile, JOB)
assert r.dimension_scores == {"required": 50.0, "preferred": 50.0,
"experience": 50.0, "scenario": 50.0}
# 总分 = 0.5*50 + 0.15*50 + 0.2*50 + 0.15*50 = 50
assert r.total_score == 50.0
assert not r.is_match # < 60 阈值
def test_custom_weights_override():
profile = ResumeProfile(skills=["Java", "Spring Boot", "MySQL", "Redis"],
years=3, scenarios=["电商", "金融"])
r = _engine(required=1.0, preferred=0.0, experience=0.0, scenario=0.0).match(profile, JOB)
assert r.total_score == 100.0 and r.is_match
def test_gap_list_and_transferable():
profile = ResumeProfile(skills=["Java", "Spring", "MySQL"], years=3,
scenarios=["电商", "金融"])
r = _engine().match(profile, JOB)
assert r.missing_required == ["Spring Boot", "Redis"]
assert r.matched_required == ["Java", "MySQL"]
# Spring 与缺失的 Spring Boot 同属“后端框架”→ 可迁移
assert "Spring" in r.transferable_skills
def test_learning_path_topological_order():
"""缺失技能按前置关系拓扑分层:Spring → Spring Boot → Spring Cloud。"""
lexicon = load_lexicon()
stages = topological_stages(["Spring Cloud", "Spring Boot", "Spring"], lexicon)
assert stages[0]["skills"] == ["Spring"]
assert stages[1]["skills"] == ["Spring Boot"]
assert stages[2]["skills"] == ["Spring Cloud"]
# 全部缺失时拓扑序稳定且不漏项
stages2 = topological_stages(["Kubernetes", "Docker", "Linux"], lexicon)
flat = [s for st in stages2 for s in st["skills"]]
assert flat == ["Linux", "Docker", "Kubernetes"]
def test_learning_path_acceptance():
"""每个阶段附带结构化验收标准:产出物/量化验收项/自测问答,且按技能类别差异化。"""
lexicon = load_lexicon()
stages = topological_stages(["Spring Cloud", "Spring Boot", "Spring"], lexicon)
for st in stages:
acc = st["acceptance"]
assert acc["deliverable"] and len(acc["criteria"]) >= 3 and len(acc["self_check"]) >= 2
assert all("{" not in c for c in acc["criteria"] + acc["self_check"]) # 占位符全部替换
assert any(s in acc["deliverable"] for s in st["skills"]) # 产出物点名本阶段技能
# 类别差异化:后端框架类与产品类的验收内容应不同
be = topological_stages(["Spring"], lexicon)[0]["acceptance"]
pm = topological_stages(["A/B测试"], lexicon)[0]["acceptance"]
assert be["category"] != pm["category"] or be["criteria"] != pm["criteria"]
def test_resume_masking():
text = "姓名:王小明\n手机:13812345678\n邮箱:wxm@example.com\n3年开发经验"
masked = mask_text(text)
assert "王小明" not in masked and "王**" in masked
assert "13812345678" not in masked and "138****78" in masked
assert "wxm@example.com" not in masked and "w***@example.com" in masked
def test_resume_parse_skills_and_years():
profile = parse_resume("姓名:李四\n5年后端开发经验,熟悉Java、MySQL与Docker。")
assert profile.years == 5.0
assert set(profile.skills) == {"Java", "MySQL", "Docker"}
assert "138" not in profile.raw_masked_text