Manually running a script proves something about one run. A useful automated test records an expectation that can be checked after every change. After files and JSON, you have enough moving parts to benefit from both small calculation tests and a real file integration test. Python includes unittest, so the examples need no third-party test runner.
Start with observable behavior
A unit test checks a small piece of behavior in isolation. An integration test checks cooperating pieces, such as a file reader and JSON parser. The exact boundary varies by project; the useful question is what each test would catch.
Create a new testing-demo directory with session_stats.py and test_session_stats.py. Keep tests independent of the final project's storage schema; this example deliberately stores just a list of durations.
Put this implementation in session_stats.py:
import json
from pathlib import Path
def total_minutes(values: object) -> int:
if not isinstance(values, list):
raise ValueError("durations must be a list")
total = 0
for value in values:
if type(value) is not int or value < 0:
raise ValueError("durations must be non-negative integers")
total += value
return total
def total_from_file(path: Path) -> int:
return total_minutes(json.loads(path.read_text(encoding="utf-8")))
Now put these tests in test_session_stats.py:
import unittest
from pathlib import Path
from tempfile import TemporaryDirectory
from session_stats import total_from_file, total_minutes
class TotalTests(unittest.TestCase):
def test_multiple_sessions(self):
self.assertEqual(total_minutes([10, 20, 5]), 35)
def test_empty_and_zero(self):
self.assertEqual(total_minutes([]), 0)
self.assertEqual(total_minutes([0]), 0)
def test_invalid_durations(self):
for values in [[-1], [True], ["20"], {"minutes": 20}]:
with self.subTest(values=values):
with self.assertRaises(ValueError):
total_minutes(values)
def test_reads_real_json_file(self):
with TemporaryDirectory() as directory:
path = Path(directory) / "durations.json"
path.write_text("[10, 20, 5]", encoding="utf-8")
self.assertEqual(total_from_file(path), 35)
if __name__ == "__main__":
unittest.main()
From that directory, run:
python -m unittest discover -s . -p "test_*.py" -v
Expect four tests and an OK result. The elapsed time varies. A test case is a method discovered on a TestCase subclass, normally named with a test_ prefix. subTest labels several related inputs without turning each input into a separate discovered method. Discovery and assertion helpers are documented in the unittest reference.
Read a failure as a useful report
Each test follows arrange, act, assert: prepare inputs, call the operation, compare behavior with the expected result. The integration test creates a real file and removes it afterward. Its fixture, the setup needed for the test, is isolated from any learner's actual session file.
Temporarily change the expected total from 35 to 36. Run the tests and inspect the failure: it should identify the failing assertion and the differing values. Restore the correct expectation. Next, remove the negative-value check from the implementation and confirm that the invalid-input test catches the regression before restoring it. Deliberately observing a failure helps establish that a test is exercising the behavior you intended.
Test names should describe outcomes, not internal variable names. Avoid asserting the exact number of loop iterations or copying the same calculation into the expected result; those approaches can preserve an implementation mistake instead of detecting it. For a small example, independently calculated expected values are easier to trust.
Keep outside dependencies under control
Tests should not depend on today's date, a live public website, or execution order unless those are explicitly part of an integration contract. Pass a clock or sender into code that needs one, and supply a small deterministic replacement during a unit test.
The standard library's unittest.mock can replace dependencies and record calls. Use it at meaningful boundaries, such as an email sender. Mocking every internal helper makes tests fragile. When patching is necessary, patch the name where the code under test looks it up. Keep at least one integration check for important real boundaries, because a mock cannot prove that file or network handling works.
Pitfalls and practice
Zero tests is not evidence of success. Check the reported count and verify discovery paths. Do not use a shared filename or rely on one test creating another test's input. Assertion methods remain active independently of Python's removable assert statements.
Exercise: add tests for malformed JSON and a valid JSON list containing true. Which function should raise in each case?
Solution guidance: use a separate temporary file for each input. Expect json.JSONDecodeError for broken syntax and ValueError from duration validation for [true]. Add import json to the test file. Keep the original four tests passing, and ensure the total count increases to six.

