Testing and CI/CD¶
seoslug is deterministic. Same inputs always produce the same outputs. This makes your SEO layer fully snapshot-testable.
Snapshot testing¶
Generate a payload once and assert nothing changes.
from seoslug import build_seo_payload
EXPECTED = {
"title": "My Post - My Blog",
"canonical": "https://blog.example.com/posts/my-post",
"robots": "index,follow",
"og": {
"type": "article",
"title": "My Post - My Blog",
"url": "https://blog.example.com/posts/my-post",
},
"twitter": {"card": "summary_large_image"},
}
def test_blog_post_snapshot():
entity = SEOEntity(entity_type="post", title="My Post", status="published")
payload = build_seo_payload(entity, "/posts/my-post", _config())
assert payload["title"] == EXPECTED["title"]
assert payload["canonical"] == EXPECTED["canonical"]
Use payload == EXPECTED for full payload equality (works because SEOPayload supports dict comparison):
EXPECTED_PAYLOAD = {
"title": "My Post - My Blog",
"description": "My excerpt",
"canonical": "https://blog.example.com/posts/my-post",
"robots": "index,follow",
"og": {
"type": "article",
"title": "My Post - My Blog",
"description": "My excerpt",
"url": "https://blog.example.com/posts/my-post",
"image": "https://cdn.example.com/default.jpg",
},
"twitter": {
"card": "summary_large_image",
"title": "My Post - My Blog",
"description": "My excerpt",
"image": "https://cdn.example.com/default.jpg",
},
}
def test_full_payload():
entity = SEOEntity(
entity_type="post",
title="My Post",
excerpt="My excerpt",
status="published",
)
payload = build_seo_payload(entity, "/posts/my-post", _config())
assert payload == EXPECTED_PAYLOAD
Regression testing¶
Parameterize across entity types to catch regressions:
import pytest
@pytest.mark.parametrize("entity_type,route,expected", [
("home", "/", HOME_EXPECTED),
("post", "/posts/p", POST_EXPECTED),
("page", "/about", PAGE_EXPECTED),
("video", "/videos/v", VIDEO_EXPECTED),
("taxonomy", "/topics/python", TAXONOMY_EXPECTED),
("search", "/search?q=x", SEARCH_EXPECTED),
("other", "/other", OTHER_EXPECTED),
("product", "/products/widget", PRODUCT_EXPECTED),
("organization", "/about", ORGANIZATION_EXPECTED),
("local_business", "/shop", LOCAL_BUSINESS_EXPECTED),
("faq", "/faq", FAQ_EXPECTED),
])
def test_regression_entity_type_snapshots(entity_type, route, expected):
entity = SEOEntity(
entity_type=entity_type,
title=f"{entity_type} title",
excerpt=f"{entity_type} excerpt",
status="published",
)
payload = build_seo_payload(entity, route, _config())
assert payload == expected
Store expected dicts in a fixtures module. Update intentionally when you change your SEO strategy.
CI integration¶
# .github/workflows/seo-tests.yml
name: SEO Tests
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
python: ["3.10", "3.11", "3.12"]
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python }}
- run: pip install -e .[test]
- run: pytest --cov=seoslug tests/
What to test¶
- Title and description fallback chains
- Canonical URL normalization (including sub-path and idempotency)
- Robots directive based on entity status and type
- Open Graph and Twitter card generation
- OGImage structured data (width, height, alt)
- Schema.org JSON-LD structure per entity type
- Override precedence for every field
- Tracking parameter stripping and query allowlists
- BreadcrumbList auto-generation
- SEOEntityBuilder fluent builder output
- Async builder matches sync builder
- Validation warnings (title length, description length, URL scheme)