From 4c98c28d40ac2492c26dec6e6b28a3a5929b12b0 Mon Sep 17 00:00:00 2001 From: ThomasTDS Date: Fri, 2 Oct 2026 15:29:26 -0300 Subject: [PATCH] Registra os marcadores TC-XXX a partir dos .feature e confere a matriz MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Os marcadores TC-XXX deixam de ser listados à mão no pyproject.toml e passam a ser registrados no conftest a partir das tags dos .feature. O pytest roda com --strict-markers, para que uma tag com erro de digitação quebre a coleta. Um teste novo garante que docs/test-cases.md lista exatamente os TCs dos .feature, com links para a linha certa de cada cenário. --- README.md | 4 ++-- conftest.py | 24 ++++++++++++++++++++++ docs/test-cases.md | 2 ++ pyproject.toml | 30 ++++----------------------- tests/unit/test_traceability.py | 36 +++++++++++++++++++++++++++++++++ 5 files changed, 68 insertions(+), 28 deletions(-) create mode 100644 tests/unit/test_traceability.py diff --git a/README.md b/README.md index b68ea34..82e434f 100644 --- a/README.md +++ b/README.md @@ -37,7 +37,7 @@ qa-api-python/ │ ├── common_steps.py # Steps usados por mais de uma feature (tokens, booking criado, 403, 405) │ └── booking_data.py # Booking padrão e payloads com campos inválidos ├── tests/ -│ └── unit/ # Testes unitários isolados de api/ e models/ (com requests-mock, sem rede) +│ └── unit/ # Testes unitários isolados de api/ e models/ (com requests-mock, sem rede) e checagem da matriz de test cases ├── api/ # API Clients (AuthApiClient e BookingApiClient, sobre a base comum BaseApiClient) ├── models/ # Schemas Pydantic e tipos derivados (shape dos dados da API) ├── reports/ # Relatório HTML e cobertura (coverage.xml) gerados a cada execução (não versionado) @@ -162,7 +162,7 @@ Autenticação: `POST /auth` com `{ "username": "admin", "password": "password12 - Integração contínua via GitHub Actions, em dois jobs: `unit` roda as checagens estáticas e os testes unitários, sem rede, e `integration` só começa se o `unit` passar, rodando a suíte completa contra a API pública. Assim, uma falha no `integration` com o `unit` verde aponta para a API ou para um cenário, e não para o código dos clients. Os dois rodam a cada push e pull request para `master`, em Python 3.12 e 3.13, e também diariamente às 06:00 UTC (ver [.github/workflows/tests.yml](.github/workflows/tests.yml)) para detectar quebras causadas pela própria API pública, com o relatório HTML publicado como artifact do workflow. - Dependências atualizadas automaticamente pelo Dependabot (pip e GitHub Actions, semanal — ver [.github/dependabot.yml](.github/dependabot.yml)). PRs de patch/minor com CI verde são mergeados automaticamente ([.github/workflows/dependabot-auto-merge.yml](.github/workflows/dependabot-auto-merge.yml)); bumps de major exigem revisão manual. - Auditoria de vulnerabilidades conhecidas nas dependências instaladas a cada execução do CI, via [`pip-audit`](https://github.com/pypa/pip-audit); encontrar uma vulnerabilidade quebra o build. -- Rastreabilidade de QA: matriz de test cases em [docs/test-cases.md](docs/test-cases.md), com tags `@TC-XXX` em cada `Cenário` e um subconjunto `@smoke` (`pytest -m smoke`) cobrindo os fluxos ponta-a-ponta mais críticos. Bugs reais encontrados são documentados como GitHub Issues usando o template em [.github/ISSUE_TEMPLATE/bug_report.md](.github/ISSUE_TEMPLATE/bug_report.md). +- Rastreabilidade de QA: matriz de test cases em [docs/test-cases.md](docs/test-cases.md), com tags `@TC-XXX` em cada `Cenário`. Os marcadores `TC-XXX` são registrados automaticamente a partir das tags dos `.feature`, o pytest roda com `--strict-markers` (uma tag com erro de digitação quebra a coleta), e o teste [tests/unit/test_traceability.py](tests/unit/test_traceability.py) garante que a matriz lista exatamente os TCs dos `.feature`, com links para a linha certa de cada cenário e um subconjunto `@smoke` (`pytest -m smoke`) cobrindo os fluxos ponta-a-ponta mais críticos. Bugs reais encontrados são documentados como GitHub Issues usando o template em [.github/ISSUE_TEMPLATE/bug_report.md](.github/ISSUE_TEMPLATE/bug_report.md). --- diff --git a/conftest.py b/conftest.py index 1255a5c..8d18008 100644 --- a/conftest.py +++ b/conftest.py @@ -1,7 +1,9 @@ import json import os +import re import warnings from collections.abc import Generator +from pathlib import Path from typing import Any import pytest @@ -22,6 +24,28 @@ API_PASSWORD = os.environ.get("API_PASSWORD", "password123") +FEATURES_DIR = Path(__file__).parent / "features" + + +# Cada cenário tem uma tag @TC-XXX na linha logo acima de "Cenário:". Devolve, +# para cada TC, o arquivo .feature e o número da linha do cenário. É a fonte +# usada para registrar os marcadores e para conferir a matriz em docs/. +def tc_scenarios() -> dict[str, tuple[str, int]]: + found: dict[str, tuple[str, int]] = {} + for path in sorted(FEATURES_DIR.glob("*.feature")): + lines = path.read_text(encoding="utf-8").splitlines() + for index, line in enumerate(lines): + for tc in re.findall(r"@(TC-\d{3})\b", line): + assert tc not in found, f"{tc} aparece em mais de um cenário" + found[tc] = (path.name, index + 2) + return found + + +def pytest_configure(config: pytest.Config) -> None: + for tc in tc_scenarios(): + config.addinivalue_line("markers", f"{tc}: id do test case, ver docs/test-cases.md") + + class Context: def __init__(self, session: requests.Session) -> None: self.session = session diff --git a/docs/test-cases.md b/docs/test-cases.md index 9aa5bdf..68d7ad7 100644 --- a/docs/test-cases.md +++ b/docs/test-cases.md @@ -4,6 +4,8 @@ Rastreabilidade dos casos de teste do projeto. Esta tabela **não duplica** os p Um `Esquema do Cenário` com `Exemplos` conta como um único test case (a variação de dados não infla a contagem). +A tabela é conferida automaticamente por [tests/unit/test_traceability.py](../tests/unit/test_traceability.py): todo TC dos `.feature` precisa estar aqui, e vice-versa, e cada link precisa apontar para a linha do `Cenário` correspondente. + Legenda: - **Tipo**: `Funcional` (caminho feliz) ou `Negativo` (validação de erro/bloqueio esperado) diff --git a/pyproject.toml b/pyproject.toml index a951c86..30652d3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -32,38 +32,16 @@ include = ["api*", "models*"] [tool.pytest.ini_options] testpaths = ["steps", "tests"] -addopts = "--reruns 1 --html=reports/report.html --self-contained-html --cov=api --cov=models --cov-report=term-missing --cov-report=xml:reports/coverage.xml" +addopts = "--strict-markers --reruns 1 --html=reports/report.html --self-contained-html --cov=api --cov=models --cov-report=term-missing --cov-report=xml:reports/coverage.xml" # O retry só vale para falhas de rede (conexão recusada ou timeout), que são # instabilidade da API pública. Falha de asserção ou de schema nunca é # repetida, para que um bug intermitente não passe na segunda tentativa. only_rerun = ["^(ConnectionError|ConnectTimeout|ReadTimeout):"] +# Os marcadores TC-XXX são registrados no conftest.py, a partir das tags dos +# arquivos .feature. Com --strict-markers, uma tag com erro de digitação +# quebra a coleta em vez de virar só um aviso. markers = [ "smoke: fluxos ponta-a-ponta mais críticos (pytest -m smoke)", - "TC-001: id do test case, ver docs/test-cases.md", - "TC-002: id do test case, ver docs/test-cases.md", - "TC-003: id do test case, ver docs/test-cases.md", - "TC-004: id do test case, ver docs/test-cases.md", - "TC-005: id do test case, ver docs/test-cases.md", - "TC-006: id do test case, ver docs/test-cases.md", - "TC-007: id do test case, ver docs/test-cases.md", - "TC-008: id do test case, ver docs/test-cases.md", - "TC-009: id do test case, ver docs/test-cases.md", - "TC-010: id do test case, ver docs/test-cases.md", - "TC-011: id do test case, ver docs/test-cases.md", - "TC-012: id do test case, ver docs/test-cases.md", - "TC-013: id do test case, ver docs/test-cases.md", - "TC-014: id do test case, ver docs/test-cases.md", - "TC-015: id do test case, ver docs/test-cases.md", - "TC-016: id do test case, ver docs/test-cases.md", - "TC-017: id do test case, ver docs/test-cases.md", - "TC-018: id do test case, ver docs/test-cases.md", - "TC-019: id do test case, ver docs/test-cases.md", - "TC-020: id do test case, ver docs/test-cases.md", - "TC-021: id do test case, ver docs/test-cases.md", - "TC-022: id do test case, ver docs/test-cases.md", - "TC-023: id do test case, ver docs/test-cases.md", - "TC-024: id do test case, ver docs/test-cases.md", - "TC-025: id do test case, ver docs/test-cases.md", ] [tool.ruff] diff --git a/tests/unit/test_traceability.py b/tests/unit/test_traceability.py new file mode 100644 index 0000000..d2b4bad --- /dev/null +++ b/tests/unit/test_traceability.py @@ -0,0 +1,36 @@ +import re +from pathlib import Path + +from conftest import FEATURES_DIR, tc_scenarios + +MATRIX = Path(__file__).parents[2] / "docs" / "test-cases.md" +ROW = re.compile(r"^\| (TC-\d{3}) .*\[([\w.]+):(\d+)\]\(\.\./features/([\w.]+)#L(\d+)\)") + + +def _matrix_rows() -> dict[str, tuple[str, int, str, int]]: + rows = {} + for line in MATRIX.read_text(encoding="utf-8").splitlines(): + match = ROW.match(line) + if match: + tc, text_file, text_line, link_file, link_line = match.groups() + rows[tc] = (text_file, int(text_line), link_file, int(link_line)) + return rows + + +def test_every_tc_in_the_features_is_in_the_matrix_and_vice_versa() -> None: + assert set(_matrix_rows()) == set(tc_scenarios()) + + +def test_every_matrix_link_points_to_the_line_of_its_scenario() -> None: + scenarios = tc_scenarios() + + for tc, (text_file, text_line, link_file, link_line) in _matrix_rows().items(): + expected = scenarios[tc] + assert (text_file, text_line) == expected, tc + assert (link_file, link_line) == expected, tc + + +def test_tc_tags_sit_right_above_a_scenario() -> None: + for tc, (file_name, line) in tc_scenarios().items(): + scenario = (FEATURES_DIR / file_name).read_text(encoding="utf-8").splitlines()[line - 1] + assert re.match(r"\s*(Cenário|Esquema do Cenário):", scenario), tc