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
1 change: 1 addition & 0 deletions README.en.md
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,7 @@ Authentication: `POST /auth` with `{ "username": "admin", "password": "password1
- Coverage of the health check, authentication (token and Basic auth), full CRUD, filters by name and by date, PUT vs. PATCH, and unauthorized access cases (403).
- Automated HTML report on every run (`reports/report.html`, through `pytest-html`), with the `master` version published at [thomastds.github.io/qa-api-python](https://thomastds.github.io/qa-api-python/) through GitHub Pages.
- Code coverage (`pytest-cov`) of `api/` and `models/` on every run, tracked on [Codecov](https://codecov.io/gh/ThomasTDS/qa-api-python).
- Health check before integration: a session fixture calls `GET /ping` once, before the first scenario. With the API down, each integration test shows up as an error with the message "API indisponível em <url>" ("API unavailable at <url>") and the reason, within seconds, instead of dozens of connection failures that would take minutes and look like bugs. The unit tests run normally.
- Automatic cleanup: the `context` fixture deletes the booking created in the scenario (with its own cleanup token) at the end of each test, so data does not pile up on the public API. If the deletion fails because of a network or authentication error, pytest shows a warning with the id of the booking left behind, without changing the test result.
- Unique test data: each created booking gets a random suffix in its last name (e.g. `Ciclano-3f9a1c`). Since the public API is shared, including by the parallel CI runs, searching by name finds only the test's own booking.
- Automatic retry for network failures only (`pytest-rerunfailures`, with `--reruns 1` and `only_rerun` in `pyproject.toml`): a test that fails because of a connection error or timeout runs a second time, absorbing the instability of the public demo API. Assertion or schema failures are never retried, so an intermittent bug cannot pass on the second attempt.
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,7 @@ Autenticação: `POST /auth` com `{ "username": "admin", "password": "password12
- Cobertura de health check, autenticação (token e Basic auth), CRUD completo, filtros por nome e por data, PUT vs. PATCH, e casos de acesso não autorizado (403).
- Relatório HTML automatizado a cada execução (`reports/report.html`, via `pytest-html`), com a versão da `master` publicada em [thomastds.github.io/qa-api-python](https://thomastds.github.io/qa-api-python/) via GitHub Pages.
- Cobertura de código (`pytest-cov`) de `api/` e `models/` em cada execução, acompanhada no [Codecov](https://codecov.io/gh/ThomasTDS/qa-api-python).
- Verificação de saúde antes da integração: uma fixture de sessão chama o `GET /ping` uma vez, antes do primeiro cenário. Com a API fora do ar, cada teste de integração aparece como erro com a mensagem "API indisponível em <url>" e o motivo, em segundos, em vez de dezenas de falhas de conexão que levariam minutos e pareceriam bugs. Os testes unitários rodam normalmente.
- Limpeza automática: a fixture `context` remove o booking criado no cenário (via token próprio de limpeza) ao final de cada teste, evitando acúmulo de dados na API pública. Se a remoção falhar por erro de rede ou de autenticação, o pytest mostra um aviso com o id do booking que ficou para trás, sem mudar o resultado do teste.
- Dados de teste únicos: cada booking criado leva um sufixo aleatório no sobrenome (ex: `Ciclano-3f9a1c`). Como a API pública é compartilhada, inclusive pelas execuções paralelas do CI, a busca por nome encontra só o booking do próprio teste.
- Retry automático só para falhas de rede (`pytest-rerunfailures`, com `--reruns 1` e `only_rerun` no `pyproject.toml`): um teste que falha por erro de conexão ou timeout roda uma segunda vez, amortecendo a instabilidade da API pública de demonstração. Falhas de asserção ou de schema nunca são repetidas, para que um bug intermitente não passe na segunda tentativa.
Expand Down
32 changes: 32 additions & 0 deletions conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@


FEATURES_DIR = Path(__file__).parent / "features"
STEPS_DIR = Path(__file__).parent / "steps"


# Cada cenário tem uma tag @TC-XXX na linha logo acima de "Cenário:". Devolve,
Expand Down Expand Up @@ -73,6 +74,37 @@ def token_or_empty(self) -> str:
return self.token or ""


# Verificação de saúde, feita uma vez por sessão, antes do primeiro teste de
# integração. Com a API fora do ar, cada teste de integração aparece como
# ERROR com o motivo, em vez de fazer as próprias chamadas, esperar o timeout
# e ser repetido pelo retry. O pytest guarda a falha da fixture e a repete
# nos testes seguintes, então o /ping não é chamado de novo.
@pytest.fixture(scope="session")
def api_disponivel() -> None:
problem: str | None = None
with requests.Session() as session:
try:
response = HealthApiClient(session, BASE_URL).ping()
if response.status_code != 201:
problem = f"o /ping respondeu {response.status_code}"
except requests.RequestException as error:
problem = str(error)
# Fora do except de propósito: assim a falha não carrega a exceção de
# conexão encadeada, que faria o retry (only_rerun) repetir o teste.
if problem is not None:
pytest.fail(f"API indisponível em {BASE_URL}: {problem}", pytrace=False)


# Roda na preparação de cada teste da pasta steps/ (os de integração). No
# pytest-bdd, a fixture context só é pedida pelos steps, já durante o teste;
# a verificação precisa vir antes, para a falha aparecer como ERROR. Os
# testes unitários não passam por aqui e rodam mesmo sem a API.
@pytest.fixture(autouse=True)
def _exige_api_nos_testes_de_integracao(request: pytest.FixtureRequest) -> None:
if request.node.path.is_relative_to(STEPS_DIR):
request.getfixturevalue("api_disponivel")


@pytest.fixture
def context() -> Generator[Context, None, None]:
session = requests.Session()
Expand Down
4 changes: 2 additions & 2 deletions docs/test-strategy.en.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ The tests run on Python 3.12 and 3.13, with dependencies locked in [uv.lock](../

## 6. Entry and exit criteria

**Entry:** the integration tests only start if the `unit` job passes (static checks and unit tests) and if the API answers `GET /ping`.
**Entry:** the integration tests only start if the `unit` job passes (static checks and unit tests) and if the API answers `GET /ping`. This check runs once per execution, through the `api_disponivel` fixture ([conftest.py](../conftest.py)): with no answer, each integration test shows up as an error with the reason, and the unit tests run normally.

**Exit, to merge a PR**, configured in the `master` branch protection:

Expand All @@ -77,7 +77,7 @@ The tests run on Python 3.12 and 3.13, with dependencies locked in [uv.lock](../

| Risk | Impact | Mitigation |
| ---- | ------ | ---------- |
| Public API down or slow | Failures that are not the project's fault, blocking the merge | Local API on PRs; timeout on every call; retry for network failures only |
| Public API down or slow | Failures that are not the project's fault, blocking the merge | Local API on PRs; health check that stops the integration tests with a clear message; timeout on every call; retry for network failures only |
| Data changed by third parties on the public API | Different results on every run | Local API on PRs; unique data per test; cleanup at the end of each test |
| Contract change without notice | The test passes with wrong data | Schema validation in strict mode; daily run against the public API |
| Intermittent bug hidden by the retry | A real defect passes on the second attempt | The retry only repeats connection and timeout errors, never assertion failures |
Expand Down
4 changes: 2 additions & 2 deletions docs/test-strategy.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ Os testes rodam em Python 3.12 e 3.13, com as dependências travadas no [uv.lock

## 6. Critérios de entrada e saída

**Entrada:** os testes de integração só começam se o job `unit` passar (checagens estáticas e testes unitários) e se a API responder ao `GET /ping`.
**Entrada:** os testes de integração só começam se o job `unit` passar (checagens estáticas e testes unitários) e se a API responder ao `GET /ping`. Essa verificação é feita uma vez por execução, pela fixture `api_disponivel` ([conftest.py](../conftest.py)): sem resposta, cada teste de integração aparece como erro com o motivo, e os unitários rodam normalmente.

**Saída, para mergear um PR**, configurada na proteção da branch `master`:

Expand All @@ -77,7 +77,7 @@ Os testes rodam em Python 3.12 e 3.13, com as dependências travadas no [uv.lock

| Risco | Impacto | Mitigação |
| ----- | ------- | --------- |
| API pública fora do ar ou lenta | Falhas que não são do projeto, bloqueando o merge | API local nos PRs; timeout em toda chamada; retry só para falhas de rede |
| API pública fora do ar ou lenta | Falhas que não são do projeto, bloqueando o merge | API local nos PRs; verificação de saúde que interrompe a integração com uma mensagem clara; timeout em toda chamada; retry só para falhas de rede |
| Dados alterados por terceiros na API pública | Resultado diferente a cada execução | API local nos PRs; dados únicos por teste; limpeza ao fim de cada teste |
| Mudança de contrato sem aviso | O teste passa com dados errados | Validação de schema no modo estrito; execução diária na API pública |
| Bug intermitente escondido pelo retry | Um defeito real passa na segunda tentativa | O retry só repete erros de conexão e de timeout, nunca falhas de asserção |
Expand Down
Loading