diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 6b3c812..486bcd5 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -18,3 +18,12 @@ updates: groups: actions: update-types: ['minor', 'patch'] + + # Imagem da restful-booker local (docker-compose.yml). Ela só publica a tag + # "latest", então a atualização é uma troca de digest, sem versão semver: + # o auto-merge não se aplica e o PR passa por revisão manual, o que é o + # desejado, já que uma versão nova da API pode mudar o comportamento testado. + - package-ecosystem: 'docker-compose' + directory: '/' + schedule: + interval: 'weekly' diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 552608b..91d63d7 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -60,7 +60,12 @@ jobs: # Suíte completa contra a restful-booker. Roda também os unitários de novo # (custam milissegundos) para que o relatório e a cobertura continuem # cobrindo o projeto inteiro. Uma falha aqui com o job unit verde indica - # problema na API pública ou num cenário, não no código dos clients. + # problema na API ou num cenário, não no código dos clients. + # + # PRs e pushes usam uma restful-booker local (docker-compose.yml), com a + # versão fixada: o resultado não depende do Heroku estar no ar nem de dados + # alterados por terceiros. A execução diária usa a API pública, para + # perceber quando o comportamento dela mudar. integration: needs: unit runs-on: ubuntu-latest @@ -68,6 +73,8 @@ jobs: fail-fast: false matrix: python-version: ['3.12', '3.13'] + env: + BASE_URL: ${{ github.event_name == 'schedule' && 'https://restful-booker.herokuapp.com' || 'http://localhost:3001' }} steps: - name: Checkout código @@ -84,6 +91,23 @@ jobs: - name: Instalar dependências run: uv sync --locked --extra dev + # O container leva alguns segundos para começar a responder; o /ping + # devolve 201 quando a API está pronta. + - name: Subir a restful-booker local + if: github.event_name != 'schedule' + run: | + docker compose up -d + for i in $(seq 1 30); do + if [ "$(curl -s -o /dev/null -w '%{http_code}' "$BASE_URL/ping")" = "201" ]; then + echo "API pronta em $BASE_URL" + exit 0 + fi + sleep 2 + done + echo "A API local não respondeu em 60 segundos" + docker compose logs + exit 1 + - name: Rodar testes run: uv run pytest diff --git a/README.en.md b/README.en.md index 4273d6f..d56fdf7 100644 --- a/README.en.md +++ b/README.en.md @@ -13,6 +13,8 @@ This repository contains automated tests for the public **[restful-booker](https The goal is to practice real-world API testing: authentication, full CRUD, the difference between PUT and PATCH, and authorization rules. +What is tested, how and why (scope, approach by level, environments, risks and exit criteria) is described in the [test strategy](docs/test-strategy.en.md). + The Gherkin scenarios, the test case matrix and the commit history are written in Portuguese, the project's main language. ![Test run report generated by pytest-html](docs/assets/report-example.png) @@ -34,7 +36,9 @@ qa-api-python/ │ └── dependabot.yml # Weekly dependency updates (uv and GitHub Actions) ├── docs/ │ ├── assets/ # Images used in the README -│ └── test-cases.md # Test case traceability matrix +│ ├── test-cases.md # Test case traceability matrix +│ ├── test-strategy.md # Portuguese version of the test strategy +│ └── test-strategy.en.md # Test strategy: scope, approach, environments, risks and criteria ├── features/ # Gherkin scenarios (.feature) ├── steps/ # pytest-bdd steps (integration tests, hit the real API) │ ├── test_.py # One module per .feature file, with the steps only it uses @@ -48,6 +52,7 @@ qa-api-python/ ├── conftest.py # Fixtures (per-scenario context) and failure evidence capture ├── pyproject.toml # Dependencies and configuration (pytest, ruff, mypy) ├── uv.lock # Exact versions of every dependency (generated by uv) +├── docker-compose.yml # Local restful-booker with a pinned version, used in CI and to run the tests without the public API ├── .pre-commit-config.yaml # Pre-commit hooks (ruff, mypy) ├── codecov.yml # Coverage targets checked by Codecov on PRs ├── LICENSE @@ -117,6 +122,19 @@ Codecov also blocks the PR merge (configured in `codecov.yml`) if the new or cha A pre-commit hook (the `pre-commit` framework, installed with `pre-commit install` after `uv sync`) runs `ruff --fix` and `ruff format` on staged files, and `mypy .` on the whole project, before every commit. Most lint, formatting or type problems are caught locally before they reach CI. +### Run against the local API (Docker) + +The integration tests can run against a local restful-booker, on the same version used in CI (pinned by the image digest in `docker-compose.yml`). The result then does not depend on the public API being up or on data changed by other people. With Docker running: + +``` +docker compose up -d # starts the API at http://localhost:3001 +$env:BASE_URL="http://localhost:3001"; pytest # PowerShell +BASE_URL=http://localhost:3001 pytest # bash +docker compose down # removes the container +``` + +Data is kept in memory and goes back to its initial state on every `docker compose up`. + ### Run against another environment By default the tests target `https://restful-booker.herokuapp.com` and authenticate with the public credentials from the API documentation (`admin` / `password123`). To run against another environment (for example, a local or staging instance), set these environment variables: @@ -168,8 +186,8 @@ Authentication: `POST /auth` with `{ "username": "admin", "password": "password1 - 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. -- Continuous integration with GitHub Actions, in two jobs: `unit` runs the static checks and the unit tests, with no network, and `integration` starts only if `unit` passes, running the full suite against the public API. A failure in `integration` with `unit` green therefore points to the API or a scenario, not to the clients' code. Both run on every push and pull request to `master`, on Python 3.12 and 3.13, and also daily at 06:00 UTC (see [.github/workflows/tests.yml](.github/workflows/tests.yml)) to catch breakage caused by the public API itself, with the HTML report published as a workflow artifact. -- Dependencies updated automatically by Dependabot (uv and GitHub Actions, weekly; see [.github/dependabot.yml](.github/dependabot.yml)). Patch and minor updates arrive grouped, one PR per week for each ecosystem, and are merged automatically when CI passes ([.github/workflows/dependabot-auto-merge.yml](.github/workflows/dependabot-auto-merge.yml)); major bumps require manual review. +- Continuous integration with GitHub Actions, in two jobs: `unit` runs the static checks and the unit tests, with no network, and `integration` starts only if `unit` passes, running the full suite against a local restful-booker, in a container with the version pinned in `docker-compose.yml`. A failure in `integration` with `unit` green therefore points to the API or a scenario, not to the clients' code. Both run on every push and pull request to `master`, on Python 3.12 and 3.13, and also daily at 06:00 UTC (see [.github/workflows/tests.yml](.github/workflows/tests.yml)). The daily run targets the public API, to catch changes in its behavior, with the HTML report published as a workflow artifact. +- Dependencies updated automatically by Dependabot (uv, GitHub Actions and the local API's Docker image, weekly; see [.github/dependabot.yml](.github/dependabot.yml)). Patch and minor updates arrive grouped, one PR per week for each ecosystem, and are merged automatically when CI passes ([.github/workflows/dependabot-auto-merge.yml](.github/workflows/dependabot-auto-merge.yml)); major bumps require manual review. - Audit of known vulnerabilities in the installed dependencies on every CI run, with [`pip-audit`](https://github.com/pypa/pip-audit); finding a vulnerability breaks the build. - QA traceability: test case matrix in [docs/test-cases.md](docs/test-cases.md), with `@TC-XXX` tags on every `Cenário` (scenario) and a `@smoke` subset (`pytest -m smoke`) covering the most critical end-to-end flows. The `TC-XXX` markers are registered automatically from the `.feature` tags, pytest runs with `--strict-markers` (a misspelled tag breaks collection), and the test [tests/unit/test_traceability.py](tests/unit/test_traceability.py) ensures the matrix lists exactly the TCs in the `.feature` files, with links to the right line of each scenario. Real bugs found are documented as GitHub Issues using the template in [.github/ISSUE_TEMPLATE/bug_report.md](.github/ISSUE_TEMPLATE/bug_report.md). diff --git a/README.md b/README.md index 8cc50aa..8ec3c5d 100644 --- a/README.md +++ b/README.md @@ -13,6 +13,8 @@ Este repositório contém testes automatizados da API pública **[restful-booker O objetivo é praticar testes de API "de verdade": autenticação, CRUD completo, diferença entre PUT e PATCH, e validação de regras de autorização. +O que é testado, como e por quê (escopo, abordagem por nível, ambientes, riscos e critérios de saída) está na [estratégia de testes](docs/test-strategy.md). + ![Relatório de execução dos testes gerado pelo pytest-html](docs/assets/report-example.png) *Exemplo do relatório de execução (captura de tela). O relatório ao vivo e sempre atualizado fica em [thomastds.github.io/qa-api-python](https://thomastds.github.io/qa-api-python/).* @@ -32,7 +34,9 @@ qa-api-python/ │ └── dependabot.yml # Atualização semanal de dependências (uv e GitHub Actions) ├── docs/ │ ├── assets/ # Imagens usadas no README -│ └── test-cases.md # Matriz de rastreabilidade dos test cases +│ ├── test-cases.md # Matriz de rastreabilidade dos test cases +│ ├── test-strategy.md # Estratégia de testes: escopo, abordagem, ambientes, riscos e critérios +│ └── test-strategy.en.md # Versão em inglês da estratégia de testes ├── features/ # Cenários em Gherkin (.feature) ├── steps/ # Steps do pytest-bdd (testes de integração, batem na API real) │ ├── test_.py # Um módulo por arquivo .feature, com os steps exclusivos dele @@ -46,6 +50,7 @@ qa-api-python/ ├── conftest.py # Fixtures (contexto por cenário) e captura de evidência de falha ├── pyproject.toml # Dependências e configuração (pytest, ruff, mypy) ├── uv.lock # Versões exatas de todas as dependências (gerado pelo uv) +├── docker-compose.yml # restful-booker local, com a versão fixada, usada no CI e para rodar os testes sem a API pública ├── .pre-commit-config.yaml # Hooks de pre-commit (ruff, mypy) ├── codecov.yml # Metas de cobertura verificadas pelo Codecov nos PRs ├── LICENSE @@ -115,6 +120,19 @@ O Codecov também bloqueia o merge do PR (configuração em `codecov.yml`) se o Um hook de pre-commit (framework `pre-commit`, instalado via `pre-commit install` após o `uv sync`) roda `ruff --fix` e `ruff format` nos arquivos staged, e também `mypy .` no projeto inteiro, antes de cada commit — então a maioria dos problemas de lint, formatação ou tipo já é pega localmente antes de chegar no CI. +### Rodar contra a API local (Docker) + +Os testes de integração podem rodar contra uma restful-booker local, na mesma versão usada no CI (fixada pelo digest da imagem no `docker-compose.yml`). Assim o resultado não depende da API pública estar no ar nem de dados alterados por outras pessoas. Com o Docker em execução: + +``` +docker compose up -d # sobe a API em http://localhost:3001 +$env:BASE_URL="http://localhost:3001"; pytest # PowerShell +BASE_URL=http://localhost:3001 pytest # bash +docker compose down # remove o container +``` + +Os dados ficam em memória e voltam ao estado inicial a cada `docker compose up`. + ### Rodar contra outro ambiente Por padrão os testes apontam para `https://restful-booker.herokuapp.com` e se autenticam com as credenciais públicas da documentação da API (`admin` / `password123`). Para rodar contra outro ambiente (ex: uma instância local ou de staging), defina as variáveis de ambiente: @@ -166,8 +184,8 @@ Autenticação: `POST /auth` com `{ "username": "admin", "password": "password12 - 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. -- 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 (uv e GitHub Actions, semanal — ver [.github/dependabot.yml](.github/dependabot.yml)). As atualizações de patch e minor chegam agrupadas, um PR por semana para cada ecossistema, e são mergeadas automaticamente quando o CI passa ([.github/workflows/dependabot-auto-merge.yml](.github/workflows/dependabot-auto-merge.yml)); bumps de major exigem revisão manual. +- 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 uma restful-booker local, num container com a versão fixada no `docker-compose.yml`. 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)). A execução diária roda contra a API pública, para detectar quando o comportamento dela mudar, com o relatório HTML publicado como artifact do workflow. +- Dependências atualizadas automaticamente pelo Dependabot (uv, GitHub Actions e a imagem Docker da API local, semanal — ver [.github/dependabot.yml](.github/dependabot.yml)). As atualizações de patch e minor chegam agrupadas, um PR por semana para cada ecossistema, e são mergeadas automaticamente quando o CI passa ([.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. 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. 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/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..f7b217e --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,10 @@ +# restful-booker local, para rodar os testes de integração sem depender da API +# pública. A imagem é fixada pelo digest (a imagem só publica a tag "latest"), +# para que todo mundo teste contra a mesma versão; o Dependabot abre um PR +# quando sair uma nova. Os dados ficam em memória e voltam ao estado inicial +# a cada "docker compose up". +services: + restful-booker: + image: mwinteringham/restfulbooker:latest@sha256:a87ae093264caf64cad2557256921cbdb1fec466d6356b5e140e3f060b904f04 + ports: + - '3001:3001' diff --git a/docs/test-strategy.en.md b/docs/test-strategy.en.md new file mode 100644 index 0000000..9413960 --- /dev/null +++ b/docs/test-strategy.en.md @@ -0,0 +1,104 @@ +# Test Strategy + +[Português](test-strategy.md) | **English** + +This document explains what this project tests, how, and why. Each decision points to the file that implements it, so it can be checked against the code. The Gherkin scenarios and the test case matrix it refers to are written in Portuguese. + +## 1. Objective + +Verify the behavior of the public [restful-booker](https://restful-booker.herokuapp.com) API (authentication and booking CRUD) and document the defects found, with evidence. The project is also an example of how to organize an API test suite: readable, traceable and reliable in CI. + +## 2. Scope + +**In scope:** + +- Authentication: token creation with valid and invalid credentials. +- Booking CRUD: creation, retrieval (by id and by name filter), full update (PUT), partial update (PATCH) and deletion. +- Authorization: write calls with no token and with an invalid token. +- Input validation: missing fields, wrong types, negative values and dates in an invalid format. +- Response contract: shape and type of every field. + +**Out of scope:** + +- Load and performance testing: the public API is shared, and generating load on it would hurt other people. +- Web interface: restful-booker does not have one. +- Penetration testing: the project records access control flaws that show up in the functional tests, but it does not attack the API. + +**Known gaps, planned:** the health check (`GET /ping`), the date filters (`checkin` and `checkout`) and authentication through `Authorization: Basic` have no scenarios yet. + +## 3. Approach by level + +| Level | Where | What it checks | Why | +| ----- | ----- | -------------- | --- | +| Static | CI, `unit` job | Types (`mypy --strict`), style (`ruff`) and vulnerable dependencies (`pip-audit`) | Catches errors without running anything, in seconds | +| Unit | [tests/unit/](../tests/unit/) | Whether the API Clients build each request correctly (URL, payload, cookie, timeout) and whether the models accept and reject what they should | Runs with no network, in milliseconds, and separates failures in the project's code from failures in the API | +| Integration (BDD) | [features/](../features/) and [steps/](../steps/) | The real behavior of the API, with Gherkin scenarios | The scenarios work as tests and as a readable specification at the same time | +| Contract | [models/booking.py](../models/booking.py) | Each response validated against a Pydantic model in strict mode | A change in the shape or type of a response fails the test instead of going unnoticed | +| Smoke | `@smoke` tag | The 5 most critical end-to-end flows | A quick check that the essentials work (`pytest -m smoke`) | +| Documentation | [tests/unit/test_traceability.py](../tests/unit/test_traceability.py) and [tests/unit/test_translation_sync.py](../tests/unit/test_translation_sync.py) | Whether the test case matrix matches the scenarios and whether the Portuguese and English versions of the documents have the same structure | Outdated documentation is a defect that no functional test catches | + +**Techniques applied:** + +- **Equivalence partitioning and invalid values:** for each field, one valid value and the relevant classes of invalid values (missing, wrong type, negative, wrong format). +- **Negative authorization tests:** every write operation is tested with no token and with an invalid token. +- **Characterization tests:** when the API has a bug, the scenario records the current behavior and points to the issue that documents it. If the API gets fixed, the scenario fails, signaling that the issue can be closed. + +## 4. Environments + +| Environment | When | Why | +| ----------- | ---- | --- | +| Local restful-booker in a container ([docker-compose.yml](../docker-compose.yml)) | PRs and pushes to `master` | Deterministic results: same version (pinned by the image digest) and same initial data on every run, with no dependency on Heroku | +| Public API (Heroku) | Daily run, at 06:00 UTC | Catch changes in the real API's behavior | +| Developer machine | On demand | Targets the public API by default; with `docker compose up -d` and `BASE_URL=http://localhost:3001`, it uses the local one | + +The tests run on Python 3.12 and 3.13, with dependencies locked in [uv.lock](../uv.lock). + +## 5. Test data + +- Each scenario creates its own data from a default booking ([steps/booking_data.py](../steps/booking_data.py)) and changes only the field relevant to the case. +- The last name gets a random suffix, so a search by name finds only the test's own booking. +- At the end of each test, the `context` fixture ([conftest.py](../conftest.py)) deletes the booking it created. If the deletion fails, a warning shows the id left behind. +- No scenario depends on data created by another, so execution order does not matter. + +## 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`. + +**Exit, to merge a PR**, configured in the `master` branch protection: + +- `unit` and `integration` passing on Python 3.12 and 3.13. +- New or changed code in `api/` and `models/` 100% covered (`codecov/patch`). +- No known vulnerability in the dependencies. +- The test case matrix and the translations in sync, checked by the documentation tests. + +**Daily run:** a failure against the public API gets investigated. If it is a change in the API's behavior, it becomes an issue or updates an existing one, and the scenario is adjusted. + +## 7. Risks and mitigation + +| 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 | +| 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 | +| Local API version different from the public one | A PR passes and the real API behaves differently | Image pinned by digest; Dependabot flags new versions; the daily run uses the public API | +| New dependency breaking CI or carrying a vulnerability | Red CI with no change in the project | Versions locked in `uv.lock`; `pip-audit` on every CI run; weekly Dependabot | +| Outdated documentation | Matrix or README describing something that does not exist | Traceability and translation sync tests | +| Daily run failing without anyone noticing | A problem goes untreated for days (it happened from Oct 21 to 24, 2026) | Planned: open an issue automatically when the daily run fails | + +## 8. Defect management + +- Each real defect in the API becomes a GitHub issue, using the [bug template](../.github/ISSUE_TEMPLATE/bug_report.md): steps to reproduce, expected and actual result, and evidence. +- The template separates **severity** (technical impact: Blocker, Critical, Major, Minor) from **priority** (urgency to fix: P0 to P3), because a defect can be serious and not urgent, or the other way around. +- The scenario that found the defect cites the issue in a comment in the `.feature` file and records the current behavior (section 3, characterization tests). +- When a test fails in CI, the report shows the URL, status, headers and body of the last response, which already serves as evidence for the issue. + +So far, 7 defects are documented, all of them about input validation or status codes. For example: the API returns 500 when a required field is missing ([#8](https://github.com/ThomasTDS/qa-api-python/issues/8)) and accepts a negative price ([#21](https://github.com/ThomasTDS/qa-api-python/issues/21)). + +## 9. Metrics and reports + +- **Result of each run:** HTML report published on [GitHub Pages](https://thomastds.github.io/qa-api-python/) on every push to `master`, and kept as an artifact on every PR. +- **Code coverage:** of `api/` and `models/`, tracked on [Codecov](https://codecov.io/gh/ThomasTDS/qa-api-python). +- **Test cases:** 25 in the [matrix](test-cases.md), 8 functional and 17 negative, with 3 of critical priority. +- **Defects found:** 7 open issues. +- **Run time:** the full suite runs in under 1 second against the local API and in 25 to 40 seconds against the public one. diff --git a/docs/test-strategy.md b/docs/test-strategy.md new file mode 100644 index 0000000..2b5f853 --- /dev/null +++ b/docs/test-strategy.md @@ -0,0 +1,104 @@ +# Estratégia de Testes + +**Português** | [English](test-strategy.en.md) + +Este documento explica o que é testado neste projeto, como e por quê. Cada decisão aponta para o arquivo que a implementa, para que dê para conferir no código. + +## 1. Objetivo + +Verificar o comportamento da API pública [restful-booker](https://restful-booker.herokuapp.com) (autenticação e CRUD de bookings) e documentar com evidência os defeitos encontrados. O projeto também serve de modelo de como organizar uma suíte de testes de API: legível, rastreável e confiável no CI. + +## 2. Escopo + +**Dentro do escopo:** + +- Autenticação: geração de token com credenciais válidas e inválidas. +- CRUD de bookings: criação, consulta (por id e por filtro de nome), atualização completa (PUT), atualização parcial (PATCH) e remoção. +- Autorização: chamadas de escrita sem token e com token inválido. +- Validação de entrada: campos ausentes, tipos errados, valores negativos e datas em formato inválido. +- Contrato das respostas: formato e tipo de cada campo. + +**Fora do escopo:** + +- Testes de carga e desempenho: a API pública é compartilhada, e gerar carga nela prejudicaria outras pessoas. +- Interface web: a restful-booker não tem uma. +- Testes de intrusão: o projeto registra falhas de controle de acesso que aparecem nos testes funcionais, mas não ataca a API. + +**Lacunas conhecidas, planejadas:** o health check (`GET /ping`), os filtros por data (`checkin` e `checkout`) e a autenticação por `Authorization: Basic` ainda não têm cenários. + +## 3. Abordagem por nível + +| Nível | Onde | O que verifica | Por quê | +| ----- | ---- | -------------- | ------- | +| Estático | CI, job `unit` | Tipos (`mypy --strict`), estilo (`ruff`) e dependências vulneráveis (`pip-audit`) | Pega erros sem executar nada, em segundos | +| Unitário | [tests/unit/](../tests/unit/) | Se os API Clients montam cada requisição certo (URL, payload, cookie, timeout) e se os modelos aceitam e rejeitam o que devem | Roda sem rede, em milissegundos, e isola falhas do código do projeto das falhas da API | +| Integração (BDD) | [features/](../features/) e [steps/](../steps/) | O comportamento real da API, com cenários em Gherkin | Os cenários servem ao mesmo tempo de teste e de especificação legível | +| Contrato | [models/booking.py](../models/booking.py) | Cada resposta validada contra um modelo Pydantic no modo estrito | Uma mudança de formato ou de tipo na resposta falha o teste, em vez de passar sem ser notada | +| Smoke | tag `@smoke` | Os 5 fluxos mais críticos, de ponta a ponta | Uma verificação rápida de que o essencial funciona (`pytest -m smoke`) | +| Documentação | [tests/unit/test_traceability.py](../tests/unit/test_traceability.py) e [tests/unit/test_translation_sync.py](../tests/unit/test_translation_sync.py) | Se a matriz de test cases bate com os cenários e se as versões em português e inglês dos documentos têm a mesma estrutura | Documentação desatualizada é um defeito que nenhum teste funcional pega | + +**Técnicas aplicadas:** + +- **Partição de equivalência e valores inválidos:** para cada campo, um valor válido e as classes de valor inválido relevantes (ausente, tipo errado, negativo, formato errado). +- **Testes negativos de autorização:** cada operação de escrita é testada sem token e com token inválido. +- **Testes de caracterização:** quando a API tem um bug, o cenário registra o comportamento atual e aponta para a issue que o documenta. Se a API for corrigida, o cenário falha, avisando que a issue pode ser fechada. + +## 4. Ambientes + +| Ambiente | Quando | Por quê | +| -------- | ------ | ------- | +| restful-booker local, num container ([docker-compose.yml](../docker-compose.yml)) | PRs e pushes na `master` | Resultado determinístico: mesma versão (fixada pelo digest da imagem) e mesmos dados iniciais a cada execução, sem depender do Heroku | +| API pública (Heroku) | Execução diária, às 06:00 UTC | Perceber quando o comportamento da API real mudar | +| Máquina de quem desenvolve | Sob demanda | Por padrão aponta para a API pública; com `docker compose up -d` e `BASE_URL=http://localhost:3001`, usa a local | + +Os testes rodam em Python 3.12 e 3.13, com as dependências travadas no [uv.lock](../uv.lock). + +## 5. Dados de teste + +- Cada cenário cria os próprios dados a partir de um booking padrão ([steps/booking_data.py](../steps/booking_data.py)) e troca só o campo que interessa ao caso. +- O sobrenome ganha um sufixo aleatório, para que uma busca por nome encontre só o booking do próprio teste. +- Ao fim de cada teste, a fixture `context` ([conftest.py](../conftest.py)) remove o booking criado. Se a remoção falhar, um aviso mostra o id que ficou para trás. +- Nenhum cenário depende de dados criados por outro, então a ordem de execução não importa. + +## 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`. + +**Saída, para mergear um PR**, configurada na proteção da branch `master`: + +- `unit` e `integration` passando em Python 3.12 e 3.13. +- O código novo ou alterado em `api/` e `models/` 100% coberto (`codecov/patch`). +- Nenhuma vulnerabilidade conhecida nas dependências. +- A matriz de test cases e as traduções sincronizadas, verificadas pelos testes de documentação. + +**Execução diária:** uma falha contra a API pública é investigada. Se for uma mudança de comportamento da API, ela vira uma issue ou atualiza uma existente, e o cenário é ajustado. + +## 7. Riscos e mitigação + +| 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 | +| 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 | +| Versão da API local diferente da pública | Um PR passa e a API real se comporta de outro jeito | Imagem fixada pelo digest; Dependabot avisa quando sai versão nova; a execução diária usa a pública | +| Dependência nova quebrando o CI ou com vulnerabilidade | CI vermelho sem mudança no projeto | Versões travadas no `uv.lock`; `pip-audit` em todo CI; Dependabot semanal | +| Documentação desatualizada | Matriz ou README descrevendo algo que não existe | Testes de rastreabilidade e de sincronia das traduções | +| Falha na execução diária sem ninguém perceber | Um problema fica dias sem tratamento (aconteceu entre 21 e 24/10/2026) | Planejado: abrir uma issue automaticamente quando a execução diária falhar | + +## 8. Gestão de defeitos + +- Cada defeito real da API vira uma issue no GitHub, com o [template de bug](../.github/ISSUE_TEMPLATE/bug_report.md): passos para reproduzir, resultado esperado e obtido, e evidência. +- O template separa **severidade** (impacto técnico: Blocker, Critical, Major, Minor) de **prioridade** (urgência de correção: P0 a P3), porque um defeito pode ser grave e pouco urgente, ou o contrário. +- O cenário que encontrou o defeito cita a issue num comentário no `.feature` e registra o comportamento atual (seção 3, testes de caracterização). +- Quando um teste falha no CI, o relatório mostra a URL, o status, os headers e o corpo da última resposta, o que já serve de evidência para a issue. + +Até agora, 7 defeitos estão documentados, todos de validação de entrada ou de código de status. Por exemplo: a API devolve 500 quando falta um campo obrigatório ([#8](https://github.com/ThomasTDS/qa-api-python/issues/8)) e aceita um preço negativo ([#21](https://github.com/ThomasTDS/qa-api-python/issues/21)). + +## 9. Métricas e relatórios + +- **Resultado de cada execução:** relatório HTML publicado no [GitHub Pages](https://thomastds.github.io/qa-api-python/) a cada push na `master`, e guardado como artifact em todo PR. +- **Cobertura de código:** de `api/` e `models/`, acompanhada no [Codecov](https://codecov.io/gh/ThomasTDS/qa-api-python). +- **Casos de teste:** 25 na [matriz](test-cases.md), sendo 8 funcionais e 17 negativos, com 3 de prioridade crítica. +- **Defeitos encontrados:** 7 issues abertas. +- **Tempo de execução:** a suíte completa roda em menos de 1 segundo contra a API local e em 25 a 40 segundos contra a pública. diff --git a/tests/unit/test_readme_sync.py b/tests/unit/test_readme_sync.py deleted file mode 100644 index fb0e6b0..0000000 --- a/tests/unit/test_readme_sync.py +++ /dev/null @@ -1,71 +0,0 @@ -import re -from pathlib import Path - -# O README existe em português (README.md, o principal) e em inglês -# (README.en.md). A tradução em si não dá para conferir automaticamente, mas -# a estrutura dá: se alguém atualizar só um dos dois (uma seção nova, um -# comando que mudou, um link novo), estes testes falham. -ROOT = Path(__file__).parents[2] -README_PT = ROOT / "README.md" -README_EN = ROOT / "README.en.md" - -FENCE = "```" -LINK = re.compile(r"\]\(([^)\s]+)\)") - - -def _blocks_and_prose(path: Path) -> tuple[list[list[str]], list[str]]: - blocks: list[list[str]] = [] - prose: list[str] = [] - current: list[str] | None = None - for line in path.read_text(encoding="utf-8").splitlines(): - if line.startswith(FENCE): - if current is None: - current = [] - else: - blocks.append(current) - current = None - elif current is not None: - current.append(line) - else: - prose.append(line) - return blocks, prose - - -def _heading_levels(path: Path) -> list[int]: - _, prose = _blocks_and_prose(path) - return [len(line) - len(line.lstrip("#")) for line in prose if line.startswith("#")] - - -# Comentários (# ...) são traduzidos, então ficam de fora da comparação. O -# resto de cada linha (comandos, nomes de arquivo) precisa ser idêntico. -def _code_without_comments(path: Path) -> list[list[str]]: - blocks, _ = _blocks_and_prose(path) - result = [] - for block in blocks: - lines = [line.split("#", 1)[0].rstrip() if not line.lstrip().startswith("#") else "" for line in block] - result.append([line for line in lines if line]) - return result - - -# Os links de troca de idioma apontam um para o outro, então ficam de fora. -def _links(path: Path) -> list[str]: - _, prose = _blocks_and_prose(path) - links = LINK.findall("\n".join(prose)) - return sorted(link for link in links if link not in ("README.md", "README.en.md")) - - -def test_both_readmes_link_to_each_other() -> None: - assert "](README.en.md)" in README_PT.read_text(encoding="utf-8") - assert "](README.md)" in README_EN.read_text(encoding="utf-8") - - -def test_both_readmes_have_the_same_sections() -> None: - assert _heading_levels(README_PT) == _heading_levels(README_EN) - - -def test_both_readmes_have_the_same_commands() -> None: - assert _code_without_comments(README_PT) == _code_without_comments(README_EN) - - -def test_both_readmes_have_the_same_links() -> None: - assert _links(README_PT) == _links(README_EN) diff --git a/tests/unit/test_translation_sync.py b/tests/unit/test_translation_sync.py new file mode 100644 index 0000000..008c412 --- /dev/null +++ b/tests/unit/test_translation_sync.py @@ -0,0 +1,84 @@ +import re +from pathlib import Path + +import pytest + +# Alguns documentos existem em português (o principal) e em inglês. A +# tradução em si não dá para conferir automaticamente, mas a estrutura dá: se +# alguém atualizar só uma das versões (uma seção nova, um comando que mudou, +# um link novo), estes testes falham. +ROOT = Path(__file__).parents[2] +PAIRS = [ + (ROOT / "README.md", ROOT / "README.en.md"), + (ROOT / "docs" / "test-strategy.md", ROOT / "docs" / "test-strategy.en.md"), +] +PAIR_IDS = [pt.name for pt, _ in PAIRS] + +FENCE = "```" +LINK = re.compile(r"\]\(([^)\s]+)\)") + + +def _blocks_and_prose(path: Path) -> tuple[list[list[str]], list[str]]: + blocks: list[list[str]] = [] + prose: list[str] = [] + current: list[str] | None = None + for line in path.read_text(encoding="utf-8").splitlines(): + if line.startswith(FENCE): + if current is None: + current = [] + else: + blocks.append(current) + current = None + elif current is not None: + current.append(line) + else: + prose.append(line) + return blocks, prose + + +def _heading_levels(path: Path) -> list[int]: + _, prose = _blocks_and_prose(path) + return [len(line) - len(line.lstrip("#")) for line in prose if line.startswith("#")] + + +# Comentários (# ...) são traduzidos, então ficam de fora da comparação. O +# resto de cada linha (comandos, nomes de arquivo) precisa ser idêntico. +def _code_without_comments(path: Path) -> list[list[str]]: + blocks, _ = _blocks_and_prose(path) + result = [] + for block in blocks: + lines = [line.split("#", 1)[0].rstrip() if not line.lstrip().startswith("#") else "" for line in block] + result.append([line for line in lines if line]) + return result + + +# Os links de troca de idioma apontam um para o outro, então ficam de fora. A +# versão em inglês aponta para as traduções em inglês de outros documentos +# (x.en.md) onde a versão em português aponta para x.md: os dois contam como +# o mesmo destino. +def _links(path: Path, pair: tuple[Path, Path]) -> list[str]: + _, prose = _blocks_and_prose(path) + language_links = {pair[0].name, pair[1].name} + links = (link.replace(".en.md", ".md") for link in LINK.findall("\n".join(prose))) + return sorted(link for link in links if link not in language_links) + + +@pytest.mark.parametrize(("pt", "en"), PAIRS, ids=PAIR_IDS) +def test_both_versions_link_to_each_other(pt: Path, en: Path) -> None: + assert f"]({en.name})" in pt.read_text(encoding="utf-8") + assert f"]({pt.name})" in en.read_text(encoding="utf-8") + + +@pytest.mark.parametrize(("pt", "en"), PAIRS, ids=PAIR_IDS) +def test_both_versions_have_the_same_sections(pt: Path, en: Path) -> None: + assert _heading_levels(pt) == _heading_levels(en) + + +@pytest.mark.parametrize(("pt", "en"), PAIRS, ids=PAIR_IDS) +def test_both_versions_have_the_same_commands(pt: Path, en: Path) -> None: + assert _code_without_comments(pt) == _code_without_comments(en) + + +@pytest.mark.parametrize(("pt", "en"), PAIRS, ids=PAIR_IDS) +def test_both_versions_have_the_same_links(pt: Path, en: Path) -> None: + assert _links(pt, (pt, en)) == _links(en, (pt, en))