1 A function, and its unit tests
Build it yourself. Read the tutorial 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
If you start at this part, make your clone as Get the project says, and then run this command. It puts in your clone the files that this part starts from. A file that the project deleted before this part stays, so delete it if git check lists it:
git fetch --quiet --tags --force origin; git rm -rq --cached --ignore-unmatch -- :/ && git restore --source step-00 --staged --worktree -- :/The first code to check is the function power(), with its unit tests.
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.
Apply the patch to src/api-service/api/__init__.py, from the top of the project:
curl -fsSL https://github.com/rahuldave/cheesecracker/commit/bb1701fd54ba7c6aa9bd971c6651f34439f004d6.diff | git apply --include=src/api-service/api/__init__.pyApply the patch to src/api-service/api/utils.py, from the top of the project:
curl -fsSL https://github.com/rahuldave/cheesecracker/commit/bb1701fd54ba7c6aa9bd971c6651f34439f004d6.diff | git apply --include=src/api-service/api/utils.pyMake the folder src/api-service/api/, and apply both patches. __init__.py has one comment, and makes the folder a
package. In utils.py, read the function power() after the docstring of the module.
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.
Check your files against this move. The command prints nothing when they match, and otherwise shows what differs:
git check bb1701fd54baIf you are stuck, this command puts the files of this move in place of yours. A file that you made yourself stays:
git fetch --quiet --tags --force origin; git restore --source step-00 --staged -- :/ && git restore --source bb1701fd54ba --staged --worktree -- :/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 patch to src/api-service/api/__init__.py
@@ -0,0 +1 @@
+# API PackageThe patch to 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.
uv add --group test pytestThe 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.
Check your files against this move. The command prints nothing when they match, and otherwise shows what differs:
git check fc8c3bce8360If you are stuck, this command puts the files of this move in place of yours. A file that you made yourself stays:
git restore --source bb1701fd54ba --staged -- :/ && git restore --source fc8c3bce8360 --staged --worktree -- :/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 that uv add --group test pytest makes in 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.
Set the values that this change sets in pyproject.toml. Run this command from the top of the project. It reads your file with tomlkit, a library for TOML files. It copies each of these tables and values from the project's file after this change, with their comments, and keeps the rest of your file:
uv run --quiet --no-config --no-project --with tomlkit python - <<'PY'
import copy, pathlib, subprocess, tomlkit
from tomlkit.items import Comment, Table, Whitespace
path = pathlib.Path('pyproject.toml')
doc = tomlkit.parse(path.read_text(encoding='utf-8')) if path.exists() else tomlkit.document()
# The project's file after this change, from your clone
project = tomlkit.parse(subprocess.run(['git', 'show', 'ff82ac1870a7:pyproject.toml'], capture_output=True, text=True,
check=True).stdout)
# Each table and value that the change sets, by its keys: each comes from the project's file with its comments
COPY = [
('tool', 'pytest', 'ini_options'),
]
def body(table): # the items of a table in their order, with its comment lines
inner = table if isinstance(table, tomlkit.TOMLDocument) else getattr(table, 'value', None)
return getattr(inner, 'body', [])
def above(table, key): # the comment lines just above a key
found = []
for name, item in body(table):
if name is not None and name.key == key:
return found
found = found + [item] if isinstance(item, Comment) else found if isinstance(item, Whitespace) else []
return []
for keys in COPY:
source, target = project, doc
for key in keys[:-1]: # make each table on the way
source = source[key]
if key not in target:
target[key] = tomlkit.table(True)
target = target[key]
item = copy.deepcopy(source[keys[-1]])
if isinstance(item, Table) and not item.as_string().endswith('\n\n'):
item.add(tomlkit.nl()) # a blank line before the table after it
if keys[-1] not in target:
for comment in above(source, keys[-1]):
target.add(copy.deepcopy(comment))
target[keys[-1]] = item
path.write_text(tomlkit.dumps(doc), encoding='utf-8')
PYRead the patch. It adds the table [tool.pytest.ini_options] to pyproject.toml.
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.
Check your files against this move. The command prints nothing when they match, and otherwise shows what differs:
git check ff82ac1870a7If you are stuck, this command puts the files of this move in place of yours. A file that you made yourself stays:
git restore --source fc8c3bce8360 --staged -- :/ && git restore --source ff82ac1870a7 --staged --worktree -- :/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 patch to 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.
Apply the patch to tests/unit/test_utils.py, from the top of the project:
curl -fsSL https://github.com/rahuldave/cheesecracker/commit/cbc98b162671bce7a7ba82d7d18c7dc6d530b746.diff | git apply --include=tests/unit/test_utils.pyMake the folder tests/unit/, and apply the patch. Read the class TestPower. Each method is one test, and each
assert compares a result of power() with its answer.
uv run pytest -qpytest prints 11 passed. So power() gives the answer that each test expects.
Check your files against this move. The command prints nothing when they match, and otherwise shows what differs:
git check cbc98b162671If you are stuck, this command puts the files of this move in place of yours. A file that you made yourself stays:
git restore --source ff82ac1870a7 --staged -- :/ && git restore --source cbc98b162671 --staged --worktree -- :/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 patch to 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.
Apply the patch to justfile, from the top of the project:
curl -fsSL https://github.com/rahuldave/cheesecracker/commit/e86e0c3d10bbe9409674f1c65ce4afdc38e7f76f.diff | git apply --include=justfileRead the recipe test at the end of the justfile.
just test -qjust prints the command of the recipe, and pytest prints 11 passed.
Check your files against this move. The command prints nothing when they match, and otherwise shows what differs:
git check e86e0c3d10bbIf you are stuck, this command puts the files of this move in place of yours. A file that you made yourself stays:
git fetch --quiet --tags --force origin; git restore --source cbc98b162671 --staged -- :/ && git restore --source step-01 --staged --worktree -- :/What changed
justfile: the recipetest. 4 lines added. The file- look at
uv run pytest "$@"
- look at
The patch to 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