Python Tutorial / Write Automated Tests with unittest and Boundary Cases

Automated tests repeatedly check defined results or exceptions for selected inputs. Test the behavior users require, including boundaries and failures, so a later implementation change can reveal a regression.

Define the rules before testing

The total function accepts a list of nonnegative integers, returns zero for an empty list and rejects bool and strings. Start with a small calculation independent of the clock, network and user files to keep its tests reproducible.

unittest belongs to the standard library. Name test files and test methods with the test_ prefix. Unlike a person inspecting printed output, assertions turn a mismatch into a machine-visible failed test.

For the relevant APIs and platform requirements, see Python: unittest.

Integration checks involving files and databases build on the rules demonstrated in Final Project: Build an SQLite Expense Manager.

Save the complete function and test files

In a fresh folder, save expense_math.py:

def total(amounts):
    if any(type(x) is not int or x < 0 for x in amounts):
        raise ValueError("invalid amount")
    return sum(amounts)

Save test_expense_math.py alongside it:

import unittest
from expense_math import total

class TotalTests(unittest.TestCase):
    def test_regular(self):
        self.assertEqual(total([3500, 8000, 2500]), 14000)

    def test_empty(self):
        self.assertEqual(total([]), 0)

    def test_invalid(self):
        for values in [[-1], [True], ["8000"]]:
            with self.subTest(values=values):
                with self.assertRaises(ValueError):
                    total(values)

if __name__ == "__main__":
    unittest.main()

Run from that folder:

python -m unittest -v

Expect three tests and a final OK. The invalid-input test uses subTest to distinguish three cases. Timing and exact layout vary by environment.

Confirm the tests detect a faulty implementation

  1. Temporarily return sum(amounts) + 1 and verify that regular and empty-input tests fail.
  2. Restore the result and check for OK.
  3. Remove input validation, inspect failures for invalid cases and then restore it.

A deliberate mutation checks that a test actually protects its intended rule. Do not leave the intentionally broken implementation as the final example.

Keep unit tests distinct from integration checks

A passing total test cannot establish that a CSV header or database connection is correct. Integration tests connecting files or databases need independent temporary paths and controlled data rather than production records.

Record the interpreter version, command and results when reporting verification. State which inputs were tested rather than extending the claim to installation screens or unrelated environments. The CSV project offers concrete file-level integration checks.

Unit tests isolate a function’s rules; integration tests check the command line, files and exit status together. Place the CSV project’s csv_report.py beside check_csv.py below, then run it. The test uses a temporary folder to check duplicate-ID rejection and report contents without overwriting reports in the working project, and cleans that folder afterward.

Save the complete example as check_csv.py and run python check_csv.py in the same practice directory. First place csv_report.py in that directory too.

import subprocess
import sys
import tempfile
from pathlib import Path
program = Path("csv_report.py").resolve()
with tempfile.TemporaryDirectory() as folder:
    work = Path(folder)
    source = work / "expenses-input.csv"
    source.write_text("id,category,amount\ne1,food,8000\ne1,food,8000\n", encoding="utf-8")
    result = subprocess.run([sys.executable, str(program)], cwd=work, capture_output=True, text=True)
    assert result.returncode == 1
    assert "food,8000" in (work / "report.csv").read_text(encoding="utf-8")
    assert "duplicate id" in (work / "rejected.csv").read_text(encoding="utf-8")
print("CSV integration checks passed")

Expected output:

CSV integration checks passed