1 A function, and its unit tests
Read the tutorial. Build it yourself is the other way to read this part.
This walk goes through rahuldave/cheesecracker on GitHub. This part starts from the code at step-00. The code before this part
To look at this code on your machine, run these commands. The first one makes a clone, if this folder has none:
[ -d cheesecracker ] || git clone https://github.com/rahuldave/cheesecracker
cd cheesecracker
git fetch --quiet --tags --force origin
git switch --detach step-00A unit test calls one function alone. The first function of the server, power(), gets eleven of them.
just setupThe server will compute distances, and it needs a function that raises a number to a power. We write the function first, and tests that call it alone. A unit test calls one function, with no server and no network, so it runs in milliseconds.
1.1 power(), the first function of the API
The code of the API is a Python package, api, in the folder src/api-service/. Its first function, power(),
returns its first argument raised to the power of its second.
± See the new file src/api-service/api/__init__.py
± See the new file src/api-service/api/utils.py
The new file src/api-service/api/utils.py has a docstring and one function, power(base, exponent). It returns
base**exponent. src/api-service/api/__init__.py has one comment, and makes api a package.
The package is not on the import path of the project yet. So the check runs Python in the folder src/api-service/,
where api is a folder of its own.
uv run --directory src/api-service python -c "from api.utils import power; print(power(2, 3))"Python prints 8, which is 2 to the power of 3.
What changed
src/api-service/api/__init__.py: one comment, which makes the folder a package. New, 1 line. The filesrc/api-service/api/utils.py: the functionpower(). New, 9 lines. The file- look at
return base**exponent
- look at
The diff of src/api-service/api/__init__.py
@@ -0,0 +1 @@
+# API PackageThe diff of src/api-service/api/utils.py
@@ -0,0 +1,9 @@
+"""
+Utility functions for mathematical operations
+Used by the Cheese App API Service
+"""
+
+
+def power(base, exponent):
+ """Calculate base raised to the power of exponent"""
+ return base**exponent1.2 pytest, in the dependency group test
A test calls power() and compares the result with the answer that we expect.
pytest is the test runner of most Python projects. It finds each function whose name
starts with test_, in the files test_*.py, and runs it. A test checks a result with a plain assert. pytest
reports each failure with the values that it compared.
The tests need pytest, but the server does not. So pytest goes into a group of
dependencies of its own, test.
This change came from running uv add --group test pytest.
The command adds pytest to the group test of pyproject.toml. It writes the exact versions of pytest and of the
packages that it needs into uv.lock.
uv run pytest --versionuv prints the version of pytest, pytest 9.1.1 on the day of the class.
What changed
pyproject.toml: pytest, in the grouptest. 5 lines added. It comes fromuv add --group test pytest. The file- look at
"pytest>=9.1.1",
- look at
uv.lock: the exact version of pytest, and of the packages that it needs. 71 lines added. It comes fromuv add --group test pytest. The file
The diff of pyproject.toml
@@ -11,3 +11,8 @@ dependencies = []
package = false
# uv sync and uv run install every dependency group, not only dev
default-groups = "all"
+
+[dependency-groups]
+test = [
+ "pytest>=9.1.1",
+]1.3 pytest finds the tests, and the package api
pytest looks for tests in every folder of the project, .venv too, which is slow. Also, the tests cannot import
api, because the folder src/api-service/ is not on the import path. Two settings of pytest, in pyproject.toml, solve both problems.
± See the change to pyproject.toml
The new table [tool.pytest.ini_options] of pyproject.toml has two settings. testpaths makes pytest
look for tests in tests/ only. pythonpath puts src/api-service/ on the import path, so a test can import api.
uv run pytestpytest collects 0 items, and fails with the code 5, because there are no tests yet. It also warns that it found no
files in tests/, which does not exist yet.
What changed
pyproject.toml: the table[tool.pytest.ini_options], with the folder of the tests and the folder of the package. 5 lines added. The file- look at
pythonpath = ["src/api-service"]
- look at
The diff of pyproject.toml
@@ -16,3 +16,8 @@ default-groups = "all"
test = [
"pytest>=9.1.1",
]
+
+[tool.pytest.ini_options]
+# The tests are in tests/, and import the package api from src/api-service/
+testpaths = ["tests"]
+pythonpath = ["src/api-service"]1.4 unit tests of power()
We do not know whether power() gives the right answer for every kind of input. Eleven tests try it with integers,
zero, one, negative numbers, floats, the special values of floats, and inputs of the wrong type.
± See the new file tests/unit/test_utils.py
The new file tests/unit/test_utils.py has the class TestPower, with eleven tests. Each assert compares a result
of power() with its answer. test_power_invalid_inputs expects a TypeError for a string, None, a list or a
dictionary.
uv run pytest -qpytest prints 11 passed. So power() gives the answer that each test expects.
What changed
tests/unit/test_utils.py: eleven unit tests ofpower(). New, 91 lines. The file- look at
assert power(2, 3) == 8
- look at
The diff of tests/unit/test_utils.py
@@ -0,0 +1,91 @@
+"""
+Unit tests for utils module
+Tests the power function in isolation
+"""
+
+import pytest
+from api.utils import power
+
+
+class TestPower:
+ """Tests for the power function"""
+
+ def test_power_positive_integers(self):
+ """Test power with positive integers"""
+ assert power(2, 3) == 8
+ assert power(3, 2) == 9
+ assert power(5, 2) == 25
+ assert power(10, 2) == 100
+
+ def test_power_with_zero_exponent(self):
+ """Test power with zero exponent (any number^0 = 1)"""
+ assert power(5, 0) == 1
+ assert power(100, 0) == 1
+ assert power(0, 0) == 1
+
+ def test_power_with_one(self):
+ """Test power with base or exponent of one"""
+ assert power(1, 5) == 1
+ assert power(5, 1) == 5
+ assert power(1, 1) == 1
+
+ def test_power_negative_base(self):
+ """Test power with negative base"""
+ assert power(-2, 2) == 4
+ assert power(-3, 3) == -27
+ assert power(-2, 4) == 16
+ assert power(-5, 2) == 25
+
+ def test_power_negative_exponent(self):
+ """Test power with negative exponent (1/base^exp)"""
+ assert power(2, -1) == pytest.approx(0.5)
+ assert power(2, -2) == pytest.approx(0.25)
+ assert power(10, -1) == pytest.approx(0.1)
+
+ def test_power_with_floats(self):
+ """Test power with floating point numbers"""
+ assert power(2.5, 2) == pytest.approx(6.25)
+ assert power(1.5, 3) == pytest.approx(3.375)
+ assert power(0.5, 2) == pytest.approx(0.25)
+
+ def test_power_large_numbers(self):
+ """Test power with large numbers"""
+ assert power(10, 6) == 1000000
+ assert power(2, 10) == 1024
+
+ def test_power_fractional_exponent(self):
+ """Test power with fractional exponent (roots)"""
+ assert power(4, 0.5) == pytest.approx(2.0) # square root
+ assert power(8, 1 / 3) == pytest.approx(2.0) # cube root
+ assert power(16, 0.25) == pytest.approx(2.0) # fourth root
+
+ def test_power_special_float_values(self):
+ """Test special float values"""
+ import math
+
+ assert math.isnan(power(float("nan"), 2))
+ assert power(float("inf"), 2) == float("inf")
+ assert power(2, float("inf")) == float("inf")
+
+ def test_power_invalid_inputs(self):
+ """Test that invalid inputs raise TypeError"""
+ with pytest.raises(TypeError):
+ power("3", 4)
+ with pytest.raises(TypeError):
+ power(3, "4")
+ with pytest.raises(TypeError):
+ power(None, 4)
+ with pytest.raises(TypeError):
+ power(3, None)
+ with pytest.raises(TypeError):
+ power([3], 4)
+ with pytest.raises(TypeError):
+ power(3, [4])
+ with pytest.raises(TypeError):
+ power({}, 4)
+
+ def test_power_boolean_values(self):
+ """Test with boolean values (True=1, False=0)"""
+ assert power(True, 5) == 1
+ assert power(False, 5) == 0
+ assert power(2, True) == 21.5 just test runs the tests
The command uv run pytest is long, and every task of the project gets a recipe in the justfile, so that one
short command runs it. The recipe test runs pytest through uv, and passes on to pytest any words that you type after just test.
The recipe test, at the end of the justfile, runs uv run pytest "$@". "$@" stands for the words after
just test, so just test tests/unit runs the unit tests alone.
just test -qjust prints the command of the recipe, and pytest prints 11 passed.
What changed
justfile: the recipetest. 4 lines added. The file- look at
uv run pytest "$@"
- look at
The diff of justfile
@@ -10,3 +10,7 @@ default:
# Install Python and every dependency into .venv, exactly as the lockfile says
setup:
uv sync --locked
+
+# Run the tests. Extra arguments go to pytest: just test tests/unit
+test *args:
+ uv run pytest "$@"This part ends at the code at step-01. The code after this part