01 Learn
Manual checks are useful. Automatic checks remember.
You already ran TaskManager methods by hand in main.py. That is useful while building, but each later change can require repeating the same checks. An automatic test records one expectation and lets Python check it again whenever you run the tests.
import unittest
class TestTaskManager(unittest.TestCase):
def setUp(self):
self.manager = TaskManager()
self.task = Task("task-1", "Read the plan")
self.manager.add_task(self.task)
def test_complete_task_marks_task_complete(self):
self.manager.complete_task("task-1")
self.assertTrue(self.task.completed)
unittest is part of Python’s standard library, so no installation is needed. A class derived from unittest.TestCase groups related tests. Each method whose name starts with test_ is discovered and run.
python3 -m unittest -v
# or: python -m unittest -v
setUp() creates a fresh manager and task before every test method. This keeps tests independent: one test cannot accidentally leave changed state for the next one.
↳One expectation at a time: assertEqual() compares an actual value with the expected value. assertTrue() checks that a condition is True. A failing assertion is useful information: first decide whether the code or the expectation is wrong.
Test an expected errorwith self.assertRaises(ValueError):
self.manager.get_task("missing-task")
assertRaises() passes only when the expected ValueError happens. Use it for a missing task_id after first deciding that ValueError is the project rule.
Test behavior, not implementation detailsself.assertTrue(self.task.completed)
A test for complete_task() can check that completed becomes True. It does not need to care how many internal lines the method uses.
Run tests from the project folderpython -m unittest
Put tests in a clearly named test file and run python -m unittest from the Task Manager project folder. The test runner finds methods that begin with test_.
Keep JSON tests separate from real datamanager.save_tasks(test_path)
Later, pass a small temporary path to save_tasks(path) and load_tasks(path) for JSON checks. A test should never overwrite the data/tasks.json file you use while learning.
A small testing routine
- Arrange known dataCreate the TaskManager and Task values the test needs, usually in setUp().
- Act onceCall one TaskManager method, such as complete_task() or get_task().
- Assert the resultUse assertEqual(), assertTrue(), or assertRaises() to compare the actual behavior with one clear expectation.
Common Task Manager testing mistakes
- Tests depend on an earlier testDo not assume another test already added a task. Give every test its own known starting state with setUp().
- Changing real learning dataDo not point a test at the JSON file you use in your normal project. Pass a separate path to save_tasks(path) and load_tasks(path) when you create isolated temporary test data.
- Expecting an error without checking the ruleUse assertRaises(ValueError) only when the project deliberately treats that input as invalid, such as a missing task_id.
02 Work locally
Test Your Finished Task Manager
Do this in VS Code
Continue in the extracted task_manager_starter folder from Concepts 28–30. The lesson diagrams call the project task_manager, but you should keep working in the same starter folder. Create focused tests for the behavior you already built; do not start a new project or download a new ZIP.
Your existing projecttask_manager/
├── main.py
├── models/
│ └── task.py
├── services/
│ └── task_manager.py
├── data/
│ └── tasks.json
└── tests/
├── __init__.py
└── test_task_manager.py
No new ZIP is needed. Keep your working Task Manager and add a tests package beside the application code, so test files stay separate from main.py and the services.
Build your tests step by step
- Create the tests packageIn the project root, create tests/, an empty tests/__init__.py file, and tests/test_task_manager.py. In the test file, import Task from models.task and TaskManager from services.task_manager.
from models.task import Task
from services.task_manager import TaskManager
import unittest
- Write one add_task() testUse unittest.TestCase and setUp() to make a fresh TaskManager and Task for every test. Arrange known objects, call add_task(), then assert that the manager has one task and that it is the Task you added.
class TestTaskManager(unittest.TestCase):
def setUp(self):
self.manager = TaskManager()
self.task = Task("task-1", "Read the plan")
- Test the operations you already builtAdd focused tests for get_task(), complete_task(), and delete_task(). Start with one behavior per test: find the same Task, mark completed as True, or remove the matching Task from the list.
self.manager.complete_task("task-1")
self.assertTrue(self.task.completed)
- Test expected ValueError casesUse assertRaises(ValueError) for the project rules from Concept 29: a duplicate task_id is rejected, and looking up or completing a missing task_id raises ValueError.
with self.assertRaises(ValueError):
self.manager.get_task("missing-task")
- Use a temporary JSON pathTemporaryDirectory creates an empty folder that Python removes when the with block ends. Build a test_path inside it, then pass that path to save_tasks(path) and load_tasks(path). This keeps your real data/tasks.json file safe.
from pathlib import Path
from tempfile import TemporaryDirectory
with TemporaryDirectory() as temp_dir:
test_path = Path(temp_dir) / "tasks.json"
- Check save, load, and replacementSave tasks to test_path, then load them into a new TaskManager. Check that the loaded items are Task objects. Add one temporary in-memory task before loading and verify that a successful load replaces that list rather than appending to it.
writer.save_tasks(test_path)
reader.load_tasks(test_path)
- Run the whole test suiteFrom the project root, discover every test in tests/. Read the first FAIL or ERROR carefully, then inspect the named test and its expected behavior before changing code.
python -m unittest discover -s tests
# or: python3 -m unittest discover -s tests
Arrange → Act → Assert in your project# Arrange
self.manager.add_task(self.task)
# Act
found = self.manager.get_task("task-1")
# Assert
self.assertEqual(found, self.task)
Keep each test small. This example checks only that get_task() returns the Task with the requested ID; write separate tests for completion and deletion.
Typical terminal result.......
----------------------------------------------------------------------
Ran 7 tests in 0.012s
OK
OK means every test finished and every assertion passed. FAIL means an assertion did not match its expectation. ERROR means an unexpected problem stopped a test before its assertion could finish.
↳Final project checklist: add → find → complete → delete → expected ValueError → save → load → test suite Confirm that basic operations work, expected exceptions are reported, JSON save and load work with a separate test path, tests begin independently, and the complete suite reports OK.
Before asking Sloby for help
- Fix imports before changing testsRun the command from the project root. Check that tests/__init__.py exists and that imports use models.task and services.task_manager exactly.
- Name files and methods for discoveryThe file should begin with test_ and each test method should begin with test_. Otherwise unittest may not run it.
- Read FAIL and ERROR differentlyFAIL means an assertion disagreed with the result. ERROR means the test could not finish, often because of an import, typo, or unexpected exception.
- Remove hidden test-order dependenciesIf a test passes alone but fails in the full suite, make sure setUp() creates all tasks and managers that test needs.
- Protect your real JSON filePass a temporary test_path to save_tasks(path) and load_tasks(path). Never point a test at your normal data/tasks.json file.
- Check the expected valueBefore changing assertEqual(), decide whether the implementation or the expected result is wrong. A test should describe the project rule, not hide a bug.
↳You have now combined classes, modules, JSON files, exceptions, and automatic tests in one local project. Finishing the checklist does not change your learning progress automatically; use it as evidence of what you can explain and verify.