845 lines
29 KiB
Python
845 lines
29 KiB
Python
"""
|
||
SEO诊断服务单元测试
|
||
"""
|
||
import pytest
|
||
from app.services.seo_diagnosis import (
|
||
SEODiagnosisService,
|
||
SEODiagnosisResult,
|
||
SEODimensionScore,
|
||
DiagnosisItem,
|
||
SEORecommendation,
|
||
DiagnosisStatus,
|
||
DimensionName,
|
||
TechnicalSEOData,
|
||
OnPageSEOData,
|
||
ContentQualityData,
|
||
BacklinkData,
|
||
UserExperienceData,
|
||
diagnose_technical_seo,
|
||
diagnose_on_page_seo,
|
||
diagnose_content_quality,
|
||
diagnose_backlinks,
|
||
diagnose_user_experience,
|
||
generate_recommendations,
|
||
)
|
||
|
||
|
||
class TestDiagnosisStatus:
|
||
"""诊断状态枚举测试"""
|
||
|
||
def test_status_values(self):
|
||
"""测试状态值"""
|
||
assert DiagnosisStatus.PASS == "pass"
|
||
assert DiagnosisStatus.WARNING == "warning"
|
||
assert DiagnosisStatus.FAIL == "fail"
|
||
|
||
|
||
class TestDimensionName:
|
||
"""维度名称枚举测试"""
|
||
|
||
def test_dimension_names(self):
|
||
"""测试维度名称"""
|
||
assert DimensionName.TECHNICAL_SEO == "技术SEO"
|
||
assert DimensionName.ON_PAGE_SEO == "页面SEO"
|
||
assert DimensionName.CONTENT_QUALITY == "内容质量"
|
||
assert DimensionName.BACKLINK_ANALYSIS == "外链分析"
|
||
assert DimensionName.USER_EXPERIENCE == "用户体验"
|
||
|
||
|
||
class TestDiagnosisItem:
|
||
"""诊断项数据结构测试"""
|
||
|
||
def test_create_item(self):
|
||
"""测试创建诊断项"""
|
||
item = DiagnosisItem(
|
||
name="测试项",
|
||
status=DiagnosisStatus.PASS,
|
||
description="测试描述",
|
||
suggestion="测试建议",
|
||
score=1.0,
|
||
)
|
||
assert item.name == "测试项"
|
||
assert item.status == DiagnosisStatus.PASS
|
||
assert item.score == 1.0
|
||
|
||
def test_item_with_details(self):
|
||
"""测试带详情的诊断项"""
|
||
item = DiagnosisItem(
|
||
name="测试项",
|
||
status=DiagnosisStatus.WARNING,
|
||
description="测试描述",
|
||
suggestion="测试建议",
|
||
details={"key": "value"},
|
||
)
|
||
assert item.details == {"key": "value"}
|
||
|
||
|
||
class TestSEODimensionScore:
|
||
"""维度评分数据结构测试"""
|
||
|
||
def test_create_dimension_score(self):
|
||
"""测试创建维度评分"""
|
||
dim = SEODimensionScore(
|
||
name="测试维度",
|
||
score=20.0,
|
||
max_score=25.0,
|
||
items=[],
|
||
status=DiagnosisStatus.PASS,
|
||
)
|
||
assert dim.score == 20.0
|
||
assert dim.max_score == 25.0
|
||
assert dim.percentage == 80.0
|
||
|
||
def test_percentage_calculation(self):
|
||
"""测试得分率计算"""
|
||
dim = SEODimensionScore(
|
||
name="测试维度",
|
||
score=15.0,
|
||
max_score=25.0,
|
||
items=[],
|
||
status=DiagnosisStatus.PASS,
|
||
)
|
||
assert dim.percentage == 60.0
|
||
|
||
def test_status_calculation_all_pass(self):
|
||
"""测试全部通过时的状态"""
|
||
items = [
|
||
DiagnosisItem(name="项1", status=DiagnosisStatus.PASS, description="", suggestion=""),
|
||
DiagnosisItem(name="项2", status=DiagnosisStatus.PASS, description="", suggestion=""),
|
||
]
|
||
dim = SEODimensionScore(
|
||
name="测试维度",
|
||
score=10.0,
|
||
max_score=10.0,
|
||
items=items,
|
||
status=DiagnosisStatus.PASS,
|
||
)
|
||
assert dim.status == DiagnosisStatus.PASS
|
||
|
||
def test_status_calculation_with_warnings(self):
|
||
"""测试有警告时的状态"""
|
||
items = [
|
||
DiagnosisItem(name="项1", status=DiagnosisStatus.PASS, description="", suggestion=""),
|
||
DiagnosisItem(name="项2", status=DiagnosisStatus.WARNING, description="", suggestion=""),
|
||
DiagnosisItem(name="项3", status=DiagnosisStatus.WARNING, description="", suggestion=""),
|
||
]
|
||
dim = SEODimensionScore(
|
||
name="测试维度",
|
||
score=7.0,
|
||
max_score=10.0,
|
||
items=items,
|
||
status=DiagnosisStatus.PASS,
|
||
)
|
||
assert dim.status == DiagnosisStatus.WARNING
|
||
|
||
def test_status_calculation_with_fails(self):
|
||
"""测试有失败时的状态"""
|
||
items = [
|
||
DiagnosisItem(name="项1", status=DiagnosisStatus.FAIL, description="", suggestion=""),
|
||
DiagnosisItem(name="项2", status=DiagnosisStatus.PASS, description="", suggestion=""),
|
||
DiagnosisItem(name="项3", status=DiagnosisStatus.PASS, description="", suggestion=""),
|
||
DiagnosisItem(name="项4", status=DiagnosisStatus.PASS, description="", suggestion=""),
|
||
]
|
||
dim = SEODimensionScore(
|
||
name="测试维度",
|
||
score=7.0,
|
||
max_score=10.0,
|
||
items=items,
|
||
status=DiagnosisStatus.PASS,
|
||
)
|
||
# 1个FAIL在4个项中占25%,未超过30%,但有FAIL所以是WARNING
|
||
assert dim.status == DiagnosisStatus.WARNING
|
||
|
||
def test_status_calculation_many_fails(self):
|
||
"""测试大量失败时的状态"""
|
||
items = [
|
||
DiagnosisItem(name="项1", status=DiagnosisStatus.FAIL, description="", suggestion=""),
|
||
DiagnosisItem(name="项2", status=DiagnosisStatus.FAIL, description="", suggestion=""),
|
||
DiagnosisItem(name="项3", status=DiagnosisStatus.FAIL, description="", suggestion=""),
|
||
DiagnosisItem(name="项4", status=DiagnosisStatus.PASS, description="", suggestion=""),
|
||
]
|
||
dim = SEODimensionScore(
|
||
name="测试维度",
|
||
score=5.0,
|
||
max_score=10.0,
|
||
items=items,
|
||
status=DiagnosisStatus.PASS,
|
||
)
|
||
assert dim.status == DiagnosisStatus.FAIL
|
||
|
||
|
||
class TestSEODiagnosisResult:
|
||
"""诊断结果数据结构测试"""
|
||
|
||
def test_create_result(self):
|
||
"""测试创建诊断结果"""
|
||
result = SEODiagnosisResult(
|
||
overall_score=75.0,
|
||
dimensions=[],
|
||
recommendations=[],
|
||
)
|
||
assert result.overall_score == 75.0
|
||
assert result.health_level == "good"
|
||
|
||
def test_health_level_excellent(self):
|
||
"""测试优秀等级"""
|
||
result = SEODiagnosisResult(
|
||
overall_score=85.0,
|
||
dimensions=[],
|
||
recommendations=[],
|
||
)
|
||
assert result.health_level == "excellent"
|
||
|
||
def test_health_level_good(self):
|
||
"""测试良好等级"""
|
||
result = SEODiagnosisResult(
|
||
overall_score=70.0,
|
||
dimensions=[],
|
||
recommendations=[],
|
||
)
|
||
assert result.health_level == "good"
|
||
|
||
def test_health_level_pass(self):
|
||
"""测试及格等级"""
|
||
result = SEODiagnosisResult(
|
||
overall_score=50.0,
|
||
dimensions=[],
|
||
recommendations=[],
|
||
)
|
||
assert result.health_level == "pass"
|
||
|
||
def test_health_level_danger(self):
|
||
"""测试危险等级"""
|
||
result = SEODiagnosisResult(
|
||
overall_score=30.0,
|
||
dimensions=[],
|
||
recommendations=[],
|
||
)
|
||
assert result.health_level == "danger"
|
||
|
||
def test_score_clamping(self):
|
||
"""测试分数限制"""
|
||
result = SEODiagnosisResult(
|
||
overall_score=150.0,
|
||
dimensions=[],
|
||
recommendations=[],
|
||
)
|
||
assert result.overall_score == 100.0
|
||
|
||
result = SEODiagnosisResult(
|
||
overall_score=-10.0,
|
||
dimensions=[],
|
||
recommendations=[],
|
||
)
|
||
assert result.overall_score == 0.0
|
||
|
||
def test_to_dict(self):
|
||
"""测试字典转换"""
|
||
result = SEODiagnosisResult(
|
||
overall_score=75.0,
|
||
dimensions=[],
|
||
recommendations=[],
|
||
)
|
||
d = result.to_dict()
|
||
assert d["overall_score"] == 75.0
|
||
assert d["health_level"] == "good"
|
||
assert d["health_level_label"] == "良好"
|
||
assert "dimensions" in d
|
||
assert "recommendations" in d
|
||
|
||
|
||
class TestTechnicalSEODiagnosis:
|
||
"""技术SEO诊断测试"""
|
||
|
||
def test_perfect_technical_seo(self):
|
||
"""测试完美技术SEO"""
|
||
data = TechnicalSEOData(
|
||
is_indexed=True,
|
||
crawl_errors=0,
|
||
lcp_seconds=2.0,
|
||
fid_ms=50.0,
|
||
cls_score=0.05,
|
||
has_robots_txt=True,
|
||
robots_txt_blocks_important=False,
|
||
has_sitemap=True,
|
||
sitemap_valid=True,
|
||
url_structure_normalized=True,
|
||
)
|
||
result = diagnose_technical_seo(data)
|
||
assert result.score == result.max_score
|
||
assert result.status == DiagnosisStatus.PASS
|
||
|
||
def test_indexed_fail(self):
|
||
"""测试未索引情况"""
|
||
data = TechnicalSEOData(is_indexed=False)
|
||
result = diagnose_technical_seo(data)
|
||
assert any(item.status == DiagnosisStatus.FAIL for item in result.items if item.name == "索引状态")
|
||
|
||
def test_crawl_errors_warning(self):
|
||
"""测试少量爬取错误"""
|
||
data = TechnicalSEOData(crawl_errors=3)
|
||
result = diagnose_technical_seo(data)
|
||
crawl_item = next(item for item in result.items if item.name == "爬取错误")
|
||
assert crawl_item.status == DiagnosisStatus.WARNING
|
||
|
||
def test_crawl_errors_fail(self):
|
||
"""测试大量爬取错误"""
|
||
data = TechnicalSEOData(crawl_errors=10)
|
||
result = diagnose_technical_seo(data)
|
||
crawl_item = next(item for item in result.items if item.name == "爬取错误")
|
||
assert crawl_item.status == DiagnosisStatus.FAIL
|
||
|
||
def test_core_web_vitals_pass(self):
|
||
"""测试Core Web Vitals通过"""
|
||
data = TechnicalSEOData(
|
||
lcp_seconds=2.0,
|
||
fid_ms=80.0,
|
||
cls_score=0.05,
|
||
)
|
||
result = diagnose_technical_seo(data)
|
||
cwv_items = [item for item in result.items if item.name in ["LCP", "FID", "CLS"]]
|
||
assert all(item.status == DiagnosisStatus.PASS for item in cwv_items)
|
||
|
||
def test_core_web_vitals_warning(self):
|
||
"""测试Core Web Vitals警告"""
|
||
data = TechnicalSEOData(
|
||
lcp_seconds=3.0,
|
||
fid_ms=200.0,
|
||
cls_score=0.15,
|
||
)
|
||
result = diagnose_technical_seo(data)
|
||
cwv_items = [item for item in result.items if item.name in ["LCP", "FID", "CLS"]]
|
||
assert any(item.status == DiagnosisStatus.WARNING for item in cwv_items)
|
||
|
||
def test_core_web_vitals_fail(self):
|
||
"""测试Core Web Vitals失败"""
|
||
data = TechnicalSEOData(
|
||
lcp_seconds=5.0,
|
||
fid_ms=400.0,
|
||
cls_score=0.3,
|
||
)
|
||
result = diagnose_technical_seo(data)
|
||
cwv_items = [item for item in result.items if item.name in ["LCP", "FID", "CLS"]]
|
||
assert all(item.status == DiagnosisStatus.FAIL for item in cwv_items)
|
||
|
||
def test_robots_txt_blocks_important(self):
|
||
"""测试robots.txt阻止重要页面"""
|
||
data = TechnicalSEOData(
|
||
has_robots_txt=True,
|
||
robots_txt_blocks_important=True,
|
||
)
|
||
result = diagnose_technical_seo(data)
|
||
robots_item = next(item for item in result.items if item.name == "robots.txt")
|
||
assert robots_item.status == DiagnosisStatus.FAIL
|
||
|
||
def test_missing_sitemap(self):
|
||
"""测试缺少sitemap"""
|
||
data = TechnicalSEOData(has_sitemap=False)
|
||
result = diagnose_technical_seo(data)
|
||
sitemap_item = next(item for item in result.items if item.name == "sitemap")
|
||
assert sitemap_item.status == DiagnosisStatus.FAIL
|
||
|
||
|
||
class TestOnPageSEODiagnosis:
|
||
"""页面SEO诊断测试"""
|
||
|
||
def test_perfect_on_page_seo(self):
|
||
"""测试完美页面SEO"""
|
||
data = OnPageSEOData(
|
||
has_title=True,
|
||
title_length=50,
|
||
title_keyword_stuffing=False,
|
||
has_meta_description=True,
|
||
meta_description_length=140,
|
||
h1_count=1,
|
||
h_structure_valid=True,
|
||
keyword_density=2.0,
|
||
internal_links=10,
|
||
broken_internal_links=0,
|
||
images_without_alt=0,
|
||
total_images=5,
|
||
)
|
||
result = diagnose_on_page_seo(data)
|
||
assert result.score == result.max_score
|
||
assert result.status == DiagnosisStatus.PASS
|
||
|
||
def test_missing_title(self):
|
||
"""测试缺少Title"""
|
||
data = OnPageSEOData(has_title=False)
|
||
result = diagnose_on_page_seo(data)
|
||
title_item = next(item for item in result.items if item.name == "Title标签")
|
||
assert title_item.status == DiagnosisStatus.FAIL
|
||
|
||
def test_title_too_long(self):
|
||
"""测试Title过长"""
|
||
data = OnPageSEOData(title_length=80)
|
||
result = diagnose_on_page_seo(data)
|
||
title_item = next(item for item in result.items if item.name == "Title标签")
|
||
assert title_item.status == DiagnosisStatus.WARNING
|
||
|
||
def test_keyword_stuffing(self):
|
||
"""测试关键词堆砌"""
|
||
data = OnPageSEOData(title_keyword_stuffing=True)
|
||
result = diagnose_on_page_seo(data)
|
||
title_item = next(item for item in result.items if item.name == "Title标签")
|
||
assert title_item.status == DiagnosisStatus.WARNING
|
||
|
||
def test_multiple_h1(self):
|
||
"""测试多个H1"""
|
||
data = OnPageSEOData(h1_count=3)
|
||
result = diagnose_on_page_seo(data)
|
||
h_item = next(item for item in result.items if item.name == "H标签结构")
|
||
assert h_item.status == DiagnosisStatus.WARNING
|
||
|
||
def test_broken_links_warning(self):
|
||
"""测试少量死链"""
|
||
data = OnPageSEOData(broken_internal_links=2)
|
||
result = diagnose_on_page_seo(data)
|
||
link_item = next(item for item in result.items if item.name == "内链结构")
|
||
assert link_item.status == DiagnosisStatus.WARNING
|
||
|
||
def test_broken_links_fail(self):
|
||
"""测试大量死链"""
|
||
data = OnPageSEOData(broken_internal_links=10)
|
||
result = diagnose_on_page_seo(data)
|
||
link_item = next(item for item in result.items if item.name == "内链结构")
|
||
assert link_item.status == DiagnosisStatus.FAIL
|
||
|
||
def test_images_without_alt(self):
|
||
"""测试图片缺少Alt"""
|
||
data = OnPageSEOData(
|
||
images_without_alt=3,
|
||
total_images=5,
|
||
)
|
||
result = diagnose_on_page_seo(data)
|
||
alt_item = next(item for item in result.items if item.name == "图片Alt文本")
|
||
assert alt_item.status == DiagnosisStatus.FAIL
|
||
|
||
|
||
class TestContentQualityDiagnosis:
|
||
"""内容质量诊断测试"""
|
||
|
||
def test_perfect_content_quality(self):
|
||
"""测试完美内容质量"""
|
||
data = ContentQualityData(
|
||
readability_score=80.0,
|
||
word_count=2000,
|
||
topic_coverage=0.9,
|
||
has_author_info=True,
|
||
has_publication_date=True,
|
||
last_updated_days=10,
|
||
has_citations=True,
|
||
citation_authority=0.9,
|
||
duplicate_content_ratio=0.02,
|
||
has_expert_review=True,
|
||
)
|
||
result = diagnose_content_quality(data)
|
||
assert result.score == result.max_score
|
||
assert result.status == DiagnosisStatus.PASS
|
||
|
||
def test_low_readability(self):
|
||
"""测试低可读性"""
|
||
data = ContentQualityData(readability_score=40.0)
|
||
result = diagnose_content_quality(data)
|
||
readability_item = next(item for item in result.items if item.name == "可读性")
|
||
assert readability_item.status == DiagnosisStatus.FAIL
|
||
|
||
def test_shallow_content(self):
|
||
"""测试内容深度不足"""
|
||
data = ContentQualityData(
|
||
word_count=500,
|
||
topic_coverage=0.4,
|
||
)
|
||
result = diagnose_content_quality(data)
|
||
depth_item = next(item for item in result.items if item.name == "信息深度")
|
||
assert depth_item.status == DiagnosisStatus.FAIL
|
||
|
||
def test_missing_author(self):
|
||
"""测试缺少作者信息"""
|
||
data = ContentQualityData(has_author_info=False)
|
||
result = diagnose_content_quality(data)
|
||
author_item = next(item for item in result.items if item.name == "作者资质")
|
||
assert author_item.status == DiagnosisStatus.WARNING
|
||
|
||
def test_stale_content(self):
|
||
"""测试过时内容"""
|
||
data = ContentQualityData(last_updated_days=200)
|
||
result = diagnose_content_quality(data)
|
||
freshness_item = next(item for item in result.items if item.name == "内容新鲜度")
|
||
assert freshness_item.status == DiagnosisStatus.FAIL
|
||
|
||
def test_high_duplicate_ratio(self):
|
||
"""测试高重复内容比例"""
|
||
data = ContentQualityData(duplicate_content_ratio=0.5)
|
||
result = diagnose_content_quality(data)
|
||
duplicate_item = next(item for item in result.items if item.name == "重复内容")
|
||
assert duplicate_item.status == DiagnosisStatus.FAIL
|
||
|
||
|
||
class TestBacklinkDiagnosis:
|
||
"""外链分析诊断测试"""
|
||
|
||
def test_perfect_backlinks(self):
|
||
"""测试完美外链"""
|
||
data = BacklinkData(
|
||
total_backlinks=200,
|
||
referring_domains=50,
|
||
high_authority_links=20,
|
||
toxic_links=0,
|
||
nofollow_ratio=0.3,
|
||
anchor_text_diversity=0.9,
|
||
exact_match_anchor_ratio=0.1,
|
||
)
|
||
result = diagnose_backlinks(data)
|
||
assert result.score == result.max_score
|
||
assert result.status == DiagnosisStatus.PASS
|
||
|
||
def test_few_referring_domains(self):
|
||
"""测试引用域名少"""
|
||
data = BacklinkData(referring_domains=5)
|
||
result = diagnose_backlinks(data)
|
||
domain_item = next(item for item in result.items if item.name == "引用域名")
|
||
assert domain_item.status == DiagnosisStatus.FAIL
|
||
|
||
def test_toxic_links_warning(self):
|
||
"""测试少量毒性链接"""
|
||
data = BacklinkData(
|
||
total_backlinks=100,
|
||
toxic_links=3,
|
||
)
|
||
result = diagnose_backlinks(data)
|
||
toxic_item = next(item for item in result.items if item.name == "毒性链接")
|
||
assert toxic_item.status == DiagnosisStatus.WARNING
|
||
|
||
def test_toxic_links_fail(self):
|
||
"""测试大量毒性链接"""
|
||
data = BacklinkData(
|
||
total_backlinks=50,
|
||
toxic_links=10,
|
||
)
|
||
result = diagnose_backlinks(data)
|
||
toxic_item = next(item for item in result.items if item.name == "毒性链接")
|
||
assert toxic_item.status == DiagnosisStatus.FAIL
|
||
|
||
def test_low_anchor_diversity(self):
|
||
"""测试锚文本多样性低"""
|
||
data = BacklinkData(
|
||
anchor_text_diversity=0.3,
|
||
exact_match_anchor_ratio=0.6,
|
||
)
|
||
result = diagnose_backlinks(data)
|
||
anchor_item = next(item for item in result.items if item.name == "锚文本分布")
|
||
assert anchor_item.status == DiagnosisStatus.FAIL
|
||
|
||
|
||
class TestUserExperienceDiagnosis:
|
||
"""用户体验诊断测试"""
|
||
|
||
def test_perfect_ux(self):
|
||
"""测试完美用户体验"""
|
||
data = UserExperienceData(
|
||
is_mobile_friendly=True,
|
||
mobile_viewport_set=True,
|
||
page_load_time=1.5,
|
||
has_https=True,
|
||
has_breadcrumbs=True,
|
||
conversion_path_clear=True,
|
||
has_cta=True,
|
||
form_usability=0.95,
|
||
has_search=True,
|
||
)
|
||
result = diagnose_user_experience(data)
|
||
assert result.score == result.max_score
|
||
assert result.status == DiagnosisStatus.PASS
|
||
|
||
def test_not_mobile_friendly(self):
|
||
"""测试不移动友好"""
|
||
data = UserExperienceData(is_mobile_friendly=False)
|
||
result = diagnose_user_experience(data)
|
||
mobile_item = next(item for item in result.items if item.name == "移动适配")
|
||
assert mobile_item.status == DiagnosisStatus.FAIL
|
||
|
||
def test_slow_page_load(self):
|
||
"""测试页面加载慢"""
|
||
data = UserExperienceData(page_load_time=5.0)
|
||
result = diagnose_user_experience(data)
|
||
speed_item = next(item for item in result.items if item.name == "页面速度")
|
||
assert speed_item.status == DiagnosisStatus.FAIL
|
||
|
||
def test_missing_https(self):
|
||
"""测试缺少HTTPS"""
|
||
data = UserExperienceData(has_https=False)
|
||
result = diagnose_user_experience(data)
|
||
https_item = next(item for item in result.items if item.name == "HTTPS")
|
||
assert https_item.status == DiagnosisStatus.FAIL
|
||
|
||
def test_missing_cta(self):
|
||
"""测试缺少CTA"""
|
||
data = UserExperienceData(has_cta=False)
|
||
result = diagnose_user_experience(data)
|
||
cta_item = next(item for item in result.items if item.name == "CTA")
|
||
assert cta_item.status == DiagnosisStatus.WARNING
|
||
|
||
|
||
class TestRecommendations:
|
||
"""优化建议生成测试"""
|
||
|
||
def test_generate_recommendations(self):
|
||
"""测试建议生成"""
|
||
result = SEODiagnosisResult(
|
||
overall_score=60.0,
|
||
dimensions=[
|
||
SEODimensionScore(
|
||
name="测试维度",
|
||
score=10.0,
|
||
max_score=20.0,
|
||
items=[
|
||
DiagnosisItem(
|
||
name="失败项",
|
||
status=DiagnosisStatus.FAIL,
|
||
description="描述",
|
||
suggestion="修复建议",
|
||
),
|
||
DiagnosisItem(
|
||
name="警告项",
|
||
status=DiagnosisStatus.WARNING,
|
||
description="描述",
|
||
suggestion="优化建议",
|
||
),
|
||
DiagnosisItem(
|
||
name="通过项",
|
||
status=DiagnosisStatus.PASS,
|
||
description="描述",
|
||
suggestion="保持",
|
||
),
|
||
],
|
||
status=DiagnosisStatus.WARNING,
|
||
),
|
||
],
|
||
recommendations=[],
|
||
)
|
||
|
||
recommendations = generate_recommendations(result)
|
||
assert len(recommendations) == 2
|
||
assert recommendations[0].priority == "high"
|
||
assert recommendations[1].priority == "medium"
|
||
|
||
def test_recommendations_sorted_by_priority(self):
|
||
"""测试建议按优先级排序"""
|
||
result = SEODiagnosisResult(
|
||
overall_score=50.0,
|
||
dimensions=[
|
||
SEODimensionScore(
|
||
name="维度1",
|
||
score=5.0,
|
||
max_score=10.0,
|
||
items=[
|
||
DiagnosisItem(
|
||
name="警告项",
|
||
status=DiagnosisStatus.WARNING,
|
||
description="",
|
||
suggestion="",
|
||
),
|
||
],
|
||
status=DiagnosisStatus.WARNING,
|
||
),
|
||
SEODimensionScore(
|
||
name="维度2",
|
||
score=5.0,
|
||
max_score=10.0,
|
||
items=[
|
||
DiagnosisItem(
|
||
name="失败项",
|
||
status=DiagnosisStatus.FAIL,
|
||
description="",
|
||
suggestion="",
|
||
),
|
||
],
|
||
status=DiagnosisStatus.WARNING,
|
||
),
|
||
],
|
||
recommendations=[],
|
||
)
|
||
|
||
recommendations = generate_recommendations(result)
|
||
assert recommendations[0].priority == "high"
|
||
assert recommendations[1].priority == "medium"
|
||
|
||
|
||
class TestSEODiagnosisService:
|
||
"""SEO诊断服务测试"""
|
||
|
||
@pytest.fixture
|
||
def service(self):
|
||
"""创建诊断服务实例"""
|
||
return SEODiagnosisService()
|
||
|
||
def test_full_diagnosis_with_defaults(self, service):
|
||
"""测试使用默认数据的完整诊断"""
|
||
result = service.diagnose()
|
||
assert isinstance(result, SEODiagnosisResult)
|
||
assert 0 <= result.overall_score <= 100
|
||
assert len(result.dimensions) == 5
|
||
assert isinstance(result.recommendations, list)
|
||
|
||
def test_diagnosis_returns_all_dimensions(self, service):
|
||
"""测试诊断返回所有维度"""
|
||
result = service.diagnose()
|
||
dimension_names = [dim.name for dim in result.dimensions]
|
||
assert "技术SEO" in dimension_names
|
||
assert "页面SEO" in dimension_names
|
||
assert "内容质量" in dimension_names
|
||
assert "外链分析" in dimension_names
|
||
assert "用户体验" in dimension_names
|
||
|
||
def test_diagnosis_with_custom_data(self, service):
|
||
"""测试使用自定义数据的诊断"""
|
||
technical_data = TechnicalSEOData(
|
||
is_indexed=False,
|
||
crawl_errors=10,
|
||
)
|
||
result = service.diagnose(technical_data=technical_data)
|
||
assert result.overall_score < 100
|
||
|
||
def test_diagnose_technical_only(self, service):
|
||
"""测试仅技术SEO诊断"""
|
||
result = service.diagnose_technical_only()
|
||
assert isinstance(result, SEODimensionScore)
|
||
assert result.name == DimensionName.TECHNICAL_SEO
|
||
|
||
def test_diagnose_on_page_only(self, service):
|
||
"""测试仅页面SEO诊断"""
|
||
result = service.diagnose_on_page_only()
|
||
assert isinstance(result, SEODimensionScore)
|
||
assert result.name == DimensionName.ON_PAGE_SEO
|
||
|
||
def test_diagnose_content_only(self, service):
|
||
"""测试仅内容质量诊断"""
|
||
result = service.diagnose_content_only()
|
||
assert isinstance(result, SEODimensionScore)
|
||
assert result.name == DimensionName.CONTENT_QUALITY
|
||
|
||
def test_diagnose_backlinks_only(self, service):
|
||
"""测试仅外链分析"""
|
||
result = service.diagnose_backlinks_only()
|
||
assert isinstance(result, SEODimensionScore)
|
||
assert result.name == DimensionName.BACKLINK_ANALYSIS
|
||
|
||
def test_diagnose_ux_only(self, service):
|
||
"""测试仅用户体验诊断"""
|
||
result = service.diagnose_ux_only()
|
||
assert isinstance(result, SEODimensionScore)
|
||
assert result.name == DimensionName.USER_EXPERIENCE
|
||
|
||
def test_diagnosis_with_poor_data(self, service):
|
||
"""测试使用差数据的诊断"""
|
||
technical_data = TechnicalSEOData(
|
||
is_indexed=False,
|
||
crawl_errors=20,
|
||
lcp_seconds=6.0,
|
||
fid_ms=500.0,
|
||
cls_score=0.4,
|
||
has_robots_txt=False,
|
||
has_sitemap=False,
|
||
)
|
||
on_page_data = OnPageSEOData(
|
||
has_title=False,
|
||
has_meta_description=False,
|
||
h1_count=0,
|
||
broken_internal_links=10,
|
||
)
|
||
content_data = ContentQualityData(
|
||
readability_score=30.0,
|
||
word_count=200,
|
||
last_updated_days=365,
|
||
duplicate_content_ratio=0.6,
|
||
)
|
||
backlink_data = BacklinkData(
|
||
referring_domains=2,
|
||
toxic_links=20,
|
||
anchor_text_diversity=0.2,
|
||
)
|
||
ux_data = UserExperienceData(
|
||
is_mobile_friendly=False,
|
||
page_load_time=8.0,
|
||
has_https=False,
|
||
)
|
||
|
||
result = service.diagnose(
|
||
technical_data=technical_data,
|
||
on_page_data=on_page_data,
|
||
content_data=content_data,
|
||
backlink_data=backlink_data,
|
||
ux_data=ux_data,
|
||
)
|
||
|
||
assert result.overall_score < 30
|
||
assert result.health_level == "danger"
|
||
assert len(result.recommendations) > 0
|
||
assert any(rec.priority == "high" for rec in result.recommendations)
|
||
|
||
def test_diagnosis_with_excellent_data(self, service):
|
||
"""测试使用优秀数据的诊断"""
|
||
technical_data = TechnicalSEOData(
|
||
is_indexed=True,
|
||
crawl_errors=0,
|
||
lcp_seconds=1.5,
|
||
fid_ms=50.0,
|
||
cls_score=0.03,
|
||
)
|
||
on_page_data = OnPageSEOData(
|
||
title_length=45,
|
||
meta_description_length=140,
|
||
keyword_density=2.0,
|
||
)
|
||
content_data = ContentQualityData(
|
||
readability_score=85.0,
|
||
word_count=2500,
|
||
topic_coverage=0.95,
|
||
has_expert_review=True,
|
||
last_updated_days=5,
|
||
)
|
||
backlink_data = BacklinkData(
|
||
referring_domains=50,
|
||
high_authority_links=20,
|
||
toxic_links=0,
|
||
anchor_text_diversity=0.9,
|
||
)
|
||
ux_data = UserExperienceData(
|
||
page_load_time=1.2,
|
||
form_usability=0.95,
|
||
)
|
||
|
||
result = service.diagnose(
|
||
technical_data=technical_data,
|
||
on_page_data=on_page_data,
|
||
content_data=content_data,
|
||
backlink_data=backlink_data,
|
||
ux_data=ux_data,
|
||
)
|
||
|
||
assert result.overall_score >= 80
|
||
assert result.health_level == "excellent"
|
||
|
||
def test_result_to_dict_format(self, service):
|
||
"""测试结果字典格式"""
|
||
result = service.diagnose()
|
||
d = result.to_dict()
|
||
|
||
assert "overall_score" in d
|
||
assert "health_level" in d
|
||
assert "health_level_label" in d
|
||
assert "dimensions" in d
|
||
assert "recommendations" in d
|
||
assert isinstance(d["dimensions"], list)
|
||
assert isinstance(d["recommendations"], list)
|
||
|
||
if d["dimensions"]:
|
||
dim = d["dimensions"][0]
|
||
assert "name" in dim
|
||
assert "score" in dim
|
||
assert "max_score" in dim
|
||
assert "percentage" in dim
|
||
assert "status" in dim
|
||
assert "items" in dim
|