English | 繁體中文
Automated API tests for the REST Countries API (https://restcountries.com), chosen from the public-apis list. It needs no authentication, returns rich JSON and has clear positive/negative behaviour, which makes it a good fit for value-level validation.
Base URL: https://restcountries.com/v3.1
| # | Test case | Type | Steps | Expected result | How it is validated |
|---|---|---|---|---|---|
| TC1 | Search country by name | Positive | GET /name/{country}?fields=name,capital,region,cca2 |
200; non-empty list; the requested country is present with the correct capital & region |
Assert status 200, find the record whose name.common matches, then assert its capital and region equal the known values |
| TC2 | Search unknown country | Negative | GET /name/{garbage} |
404 with {"status":404,"message":"Not Found"} |
Assert status 404 and that the error payload reports status == 404 and a "Not Found" message |
| TC3a | Look up country by ISO alpha code | Positive | GET /alpha/{code}?fields=name,region |
200; resolved country's common name matches expectation |
Assert status 200 and name.common equals the expected country (also covers case-insensitive codes) |
| TC3b | Look up by invalid alpha code | Negative | GET /alpha/{badcode} |
404 with {"status":404,"message":"Not Found"}, never a 200 |
Assert status 404 and the error payload (status == 404, "Not Found" message) — so an empty 200, an HTML error page or a changed schema is caught, not just "non-200" |
Each case is pytest.mark.parametrize-d over several inputs (countries / codes),
so the four cases above run as 12 parametrised checks while keeping the code
small — as recommended in the brief.
The brief asks to "make sure the answer is what we expected", so the tests do not stop at the HTTP status code:
- Status code — confirms the request was handled as intended (
200for valid input,404/400for invalid input). This alone is necessary but not sufficient: the API could return200with the wrong country. - Structure — confirms the payload shape (
/namereturns a list,/alphareturns a single object), so later field access is meaningful. - Value — confirms the actual data. For TC1 we assert the matched
country's
capitalandregion; for TC3 we assert the resolvedname.common. This is what proves the API returned the right answer, not just an answer.
Negative cases are validated against the error contract (status + message)
rather than just "not 200", so a regression that turns a 404 into an empty
200 would be caught.
Calls to a shared public API can fail for reasons unrelated to the behaviour
under test. The client wraps its requests in a small retry decorator
(client/retry.py, exponential backoff) that retries two transient classes:
- Transient errors —
ConnectionError/Timeout(a dropped connection, a momentary timeout). - Transient server statuses —
429, 500, 502, 503, 504(rate-limit / degradation signals). A persistent transient status is returned after the retries are exhausted (not swallowed), so a genuinely broken upstream still fails the assertion.
Crucially, 4xx statuses such as 404 are never retried — they are valid
results the negative tests assert on, so they reach the test immediately. The
retry logic and the client's status-retry policy are both covered by fast,
network-free unit tests (tests/test_retry.py, tests/test_client_retry.py),
which inject a fake session so no live calls are made.
restapi-testing/
├── conftest.py # session-scoped API client fixture
├── pyproject.toml
├── requirements.txt
├── .github/workflows/ # CI: install + pytest on push / PR
├── client/
│ ├── countries_client.py # thin HTTP client (base URL, timeout, endpoints)
│ └── retry.py # retry-on-transient-error decorator
└── tests/
├── test_search_by_name.py # TC1
├── test_search_negative.py # TC2
├── test_lookup_by_alpha.py # TC3a / TC3b
├── test_retry.py # offline unit tests for the retry decorator
└── test_client_retry.py # offline unit tests for the client's status-retry
The HTTP layer is isolated in CountriesClient: tests express what to check,
the client owns how to call the API. Switching the base URL or adding an
endpoint is a one-line change that every test inherits.
python3 -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt
pytestpytest output for the full suite (the four API cases run as 12 parametrised
checks, plus the offline retry/client unit tests — 26 checks total):
26 passed in 3.94s — expand for the full pytest output
============================= test session starts ==============================
platform darwin -- Python 3.9.6, pytest-8.4.2, pluggy-1.6.0 -- .venv/bin/python3
cachedir: .pytest_cache
rootdir: restapi-testing
configfile: pyproject.toml
testpaths: tests
collecting ... collected 26 items
tests/test_client_retry.py::test_get_retries_transient_status_then_returns_success PASSED [ 3%]
tests/test_client_retry.py::test_get_does_not_retry_client_error PASSED [ 7%]
tests/test_client_retry.py::test_get_returns_last_transient_response_when_exhausted PASSED [ 11%]
tests/test_client_retry.py::test_get_retries_each_transient_status[429] PASSED [ 15%]
tests/test_client_retry.py::test_get_retries_each_transient_status[500] PASSED [ 19%]
tests/test_client_retry.py::test_get_retries_each_transient_status[502] PASSED [ 23%]
tests/test_client_retry.py::test_get_retries_each_transient_status[503] PASSED [ 26%]
tests/test_client_retry.py::test_get_retries_each_transient_status[504] PASSED [ 30%]
tests/test_lookup_by_alpha.py::test_alpha_lookup_returns_expected_country[jp-Japan] PASSED [ 34%]
tests/test_lookup_by_alpha.py::test_alpha_lookup_returns_expected_country[FR-France] PASSED [ 38%]
tests/test_lookup_by_alpha.py::test_alpha_lookup_returns_expected_country[usa-United States] PASSED [ 42%]
tests/test_lookup_by_alpha.py::test_alpha_lookup_invalid_code_is_rejected[zz] PASSED [ 46%]
tests/test_lookup_by_alpha.py::test_alpha_lookup_invalid_code_is_rejected[000] PASSED [ 50%]
tests/test_lookup_by_alpha.py::test_alpha_lookup_invalid_code_is_rejected[xx] PASSED [ 53%]
tests/test_retry.py::test_retry_succeeds_after_transient_failures PASSED [ 57%]
tests/test_retry.py::test_retry_reraises_last_error_when_exhausted PASSED [ 61%]
tests/test_retry.py::test_retry_does_not_catch_unlisted_exceptions PASSED [ 65%]
tests/test_retry.py::test_retry_on_result_retries_then_returns_acceptable_result PASSED [ 69%]
tests/test_retry.py::test_retry_on_result_returns_last_result_when_exhausted PASSED [ 73%]
tests/test_retry.py::test_retry_on_result_returns_immediately_on_acceptable_result PASSED [ 76%]
tests/test_search_by_name.py::test_search_by_name_returns_expected_country[japan-Japan-Tokyo-Asia] PASSED [ 80%]
tests/test_search_by_name.py::test_search_by_name_returns_expected_country[france-France-Paris-Europe] PASSED [ 84%]
tests/test_search_by_name.py::test_search_by_name_returns_expected_country[brazil-Brazil-Brasília-Americas] PASSED [ 88%]
tests/test_search_negative.py::test_unknown_country_returns_404[zzzzznotacountry] PASSED [ 92%]
tests/test_search_negative.py::test_unknown_country_returns_404[1234567] PASSED [ 96%]
tests/test_search_negative.py::test_unknown_country_returns_404[notarealplace] PASSED [100%]
======================== 26 passed in 3.94s =========================
.github/workflows/ci.yml installs the dependencies and runs pytest on every
push and pull request — the API tests are deterministic and need no browser, so
they gate cleanly.