Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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).

---

Expand Down
24 changes: 24 additions & 0 deletions conftest.py
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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
Expand Down
2 changes: 2 additions & 0 deletions docs/test-cases.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
30 changes: 4 additions & 26 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
36 changes: 36 additions & 0 deletions tests/unit/test_traceability.py
Original file line number Diff line number Diff line change
@@ -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
Loading