Skip to content

asdfghj1237890/restapi-testing

Repository files navigation

REST Countries API Tests — Python + pytest

CI

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 cases

# 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.

Validation approach (and why)

The brief asks to "make sure the answer is what we expected", so the tests do not stop at the HTTP status code:

  1. Status code — confirms the request was handled as intended (200 for valid input, 404/400 for invalid input). This alone is necessary but not sufficient: the API could return 200 with the wrong country.
  2. Structure — confirms the payload shape (/name returns a list, /alpha returns a single object), so later field access is meaningful.
  3. Value — confirms the actual data. For TC1 we assert the matched country's capital and region; for TC3 we assert the resolved name.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.

Stability: transient-error retry

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 errorsConnectionError / Timeout (a dropped connection, a momentary timeout).
  • Transient server statuses429, 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.

Project structure

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.

Setup & running

python3 -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt

pytest

Example test run

pytest 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 =========================

CI

.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.

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages