Recommended resources
The freely available resources this lesson will direct you to.
Hello! Last time, you wrote a function that retrieves a post from an API and extracts a few fields from its JSON response. This lesson adds a way to check that code automatically. You’ll write pytest tests for a function’s expected output and error cases, run them from the command line, and use a failure report to find a mistake. Allow about 40 minutes, including the videos and time to run the code.
A test is another function with a clear expectation
A unit test calls a small piece of your program with controlled input and checks what happens. Unlike running your API script and inspecting its printed output, a test records the expectation so you can check it again after changing the code.
Watch pixegami’s How To Write Unit Tests in Python • Pytest Tutorial for the basic workflow. The example uses a shopping cart class; we’ll apply the same method to ordinary functions.
How To Write Unit Tests in Python • Pytest Tutorial
Watch pixegami’s demonstration of how pytest finds tests and how an assertion exposes an incorrect result.
Watch the first test. Notice that both the test file and the test function need recognizable names. Pay particular attention to the failed assertion and the values pytest displays before the code is corrected.
A useful test follows three moves: prepare an input, call the function, and assert an expected result. The assertion should check something that matters to the function’s contract. For a function that extracts three post fields, checking the entire returned dictionary is stronger than merely checking that it contains three keys.
Make the extraction logic testable without a live API
The last version of api_demo.py combined the network request and field extraction in fetch_post_summary(). Separate those responsibilities now. Replace the file with this version; the URL, status check, and selected fields remain the same:
import requests
def extract_post_summary(post):
if not isinstance(post, dict):
raise ValueError("Expected a JSON object for one post")
return {
"post_id": post["id"],
"author_id": post["userId"],
"title": post["title"],
}
def fetch_post_summary(post_id):
url = f"https://jsonplaceholder.typicode.com/posts/{post_id}"
response = requests.get(url, timeout=10)
response.raise_for_status()
return extract_post_summary(response.json())
if __name__ == "__main__":
try:
print(fetch_post_summary(1))
except (requests.exceptions.RequestException, ValueError, KeyError) as exc:
print(f"Could not load post: {exc}")
extract_post_summary() accepts an already-decoded Python value. That makes it possible to test field extraction using a dictionary you supply, rather than relying on a server. The if __name__ == "__main__": block still runs the demonstration when you execute api_demo.py directly, but does not make a request when pytest imports the file.
In the same directory, create test_api_demo.py:
from api_demo import extract_post_summary
def test_extract_post_summary_returns_selected_fields():
post = {
"id": 17,
"userId": 4,
"title": "A useful post",
"body": "This field is not needed in the summary.",
}
result = extract_post_summary(post)
assert result == {
"post_id": 17,
"author_id": 4,
"title": "A useful post",
}
Here, the input includes a body, but the expected result does not: the test checks that the function selects fields, not just that it copies its input. It also checks that userId is renamed to author_id and that the values come from this particular post.
Use the isolated environment from earlier lessons. Install pytest in that environment if needed, and add it to the project’s pinned development dependencies if you are keeping this project. From the directory containing both files, run:
python -m pip install pytest
python -m pytest -q
Pytest discovers files named test_*.py and functions named test_*. With the code above, you should see one passing test. If pytest reports that it collected zero tests, check both names and your working directory. If it reports an import error, check that api_demo.py and test_api_demo.py are in the same directory and that you ran the command there.
A passing test establishes only what that test checked. For a moment, change the expected "post_id" from 17 to 18 and rerun pytest. Its failure report should show the differing values. Restore 17 before continuing.
Check failures you intend the function to raise
A well-specified function has behavior for bad input as well as good input. Here, a JSON list is the wrong overall shape for one post, and a post without a required title is incomplete. Both should be visible to the caller, but they fail in different ways.
Return to pixegami’s tutorial for pytest.raises(). The most useful detail is where the expected-exception block begins and ends.
How To Write Unit Tests in Python • Pytest Tutorial
This portion shows how to verify an expected exception and why placing too many operations inside the exception check can hide a bug.
Watch expected errors. Focus especially on the later correction: only the operation that is supposed to raise should be inside pytest.raises().
Add these tests to test_api_demo.py:
import pytest
def test_extract_post_summary_rejects_a_list():
with pytest.raises(ValueError):
extract_post_summary([{"id": 17}])
def test_extract_post_summary_requires_title():
post = {"id": 17, "userId": 4}
with pytest.raises(KeyError):
extract_post_summary(post)
Keep the earlier import of extract_post_summary as well; import pytest can go beside it at the top of the file. The first test checks the explicit shape validation. The second checks that a required field is not silently treated as optional.
pytest.raises(KeyError) passes only if the code inside its block raises that exception. No exception—or a different exception—fails the test. Preparing post outside the block makes it clear which call is expected to fail.
Run all three tests with python -m pytest -q. While working on one failure, you can run just that test:
python -m pytest -q test_api_demo.py::test_extract_post_summary_requires_title
These are unit tests of extraction, not tests of the live request. They remain fast and repeatable even if the demonstration API is unavailable. A separate test of fetch_post_summary() would need to control its network dependency; simply calling the live endpoint in every unit test would make the results depend on something outside your code.
Choose assertions that fit the result
Exact equality is appropriate for the post summary: its strings, integer IDs, and dictionary keys have definite expected values. For a numerical function involving floating-point arithmetic, exact equality may be unnecessarily strict because some decimal fractions cannot be represented exactly in binary.
Read the short example in pytest’s Get Started guide before looking at this final pattern.
Read the official pytest guide’s example of comparing floating-point results when small rounding differences are acceptable.
Find “Compare floating-point values with pytest.approx,” below “Group multiple tests in a class” and above “Request a unique temporary directory for functional tests.” Starting with the sentence about utilities that make tests easier, read the approximation example. Focus on how the expected value is written in the assertion; you can skip the preceding examples about test classes.
For example, if an earlier numerical module has an average() function whose input is specified to be nonempty, its test could say:
import pytest
assert average([0.1, 0.2]) == pytest.approx(0.15)
pytest.approx() allows a small numerical tolerance; it is not a substitute for deciding what result the function should produce. For your post-extraction tests, keep exact equality.
Takeaways
A pytest test is a test_* function that calls your code and asserts its expected behavior. Put it in a discoverable test file, run it with python -m pytest, and read failures as evidence about which expectation did not hold. Test both representative successful input and meaningful error cases, keeping each pytest.raises() block focused on the call that should fail.
You’ve now tested reusable Python logic without making a live API call. Next, the course turns to SQL, where you’ll retrieve and organize records before bringing them into later data and machine-learning workflows.