LEARN PYTHON | Unit Tests And Test Data

Lesson objective

Write independent unittest checks for results, exceptions and state changes, using meaningful test data and isolated temporary files.

Learn

05.5 | UNIT TESTS AND TEST DATA

01 | A TEST STARTS WITH AN EXPECTATION

A program can run without crashing and still answer the wrong question. A test gives a specific input, states the expected behaviour and compares that prediction with what actually happens.

A unit test focuses on a small part such as an amount parser or total calculation. An integration test checks connected parts, for example saving validated records and reading them back. A manual system test checks the whole user workflow. All three can help, but they answer different questions.

Choose expected results from the requirement and an independent calculation. Copying the implementation’s formula into a test can repeat the same mistake and provide false confidence.

02 | USE UNITTEST TO REPORT CHECKS

unittest is included with Python. A test class inherits from unittest.TestCase; for this lesson, treat that as the framework’s required container for assertion methods. Detailed class design comes in Section 6.

A method whose name begins test_ is discovered as a test. self refers to this test instance and lets you call assertion methods such as self.assertEqual(actual, expected). The method returns no special score; a failed assertion is reported by the runner.

The main guard calls unittest.main when the file runs directly. You can also use python -m unittest discover -v to discover matching test files, normally named test_*.py.

import unittest

def double(number):
    return number * 2

class DoubleTests(unittest.TestCase):
    def test_positive(self):
        self.assertEqual(double(6), 12)

    def test_zero(self):
        self.assertEqual(double(0), 0)

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

03 | DIFFERENT ASSERTIONS ANSWER DIFFERENT QUESTIONS

assertEqual compares expected and actual values. assertTrue checks a condition. assertIsNone checks the exact absence sentinel. assertRaises checks that a specified exception occurs. Use the assertion that expresses the intended behaviour.

A function returning None for an absent search needs assertIsNone, while a legitimate index zero can be checked with assertEqual(..., 0). Printed output alone would not establish that return contract.

Ordinary assert statements are useful for quick development checks but can be removed under optimisation. unittest assertion methods provide framework checks. Neither is a substitute for validating untrusted input in the application.

04 | TEST REJECTION AS WELL AS SUCCESS

A validator’s promised behaviour includes what happens for unsuitable values. with self.assertRaises(ValueError) passes when that exception occurs and fails when the block does not raise it. An unexpected different exception is reported as an error.

A failed assertion is usually a test failure: behaviour did not match the expectation. An unexpected exception is an error in the test run. Inspect the message and traceback before deciding whether the test or implementation should change.

Check the exception type your contract promises. A division by zero is not a successful rejection if the requirement says the function should report a deliberate ValueError before performing unsuitable arithmetic.

import unittest

def parse_count(text):
    count = int(text)
    if not 0 <= count <= 10:
        raise ValueError("Use 0 to 10")
    return count

class CountTests(unittest.TestCase):
    def test_lower_endpoint(self):
        self.assertEqual(parse_count("0"), 0)

    def test_above_limit(self):
        with self.assertRaises(ValueError):
            parse_count("11")

    def test_unsuitable_text(self):
        with self.assertRaises(ValueError):
            parse_count("ten")

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

05 | SELECT DATA THAT EXPOSES THE RULE

Normal data represents ordinary accepted use. Boundary data sits at or around a rule’s limits. Invalid data breaks a rule. For a count from zero to ten inclusive, test -1, 0, 1, 9, 10 and 11, along with unsuitable text and blank input.

One test can cover several related cases using subTest. Each labelled case can fail without hiding the remaining cases in that loop. Do not treat a long list of random examples as automatically better than a small set that distinguishes the important branches.

For an amount parser, 0.01 and 10000.00 test accepted endpoints, while zero and 10000.01 test just-outside values. A third decimal tests the precision rule. These inputs target separate requirements.

06 | KEEP TESTS INDEPENDENT

A test should not need another test to run first. Fresh input data prevents an earlier mutation from changing the next test’s starting conditions. setUp runs before each test method and can prepare independent resources.

For files, TemporaryDirectory creates an isolated directory that can be cleaned afterwards. self.addCleanup registers cleanup even if later setup fails. Tests can safely create malformed practice files there without touching a real tracker save.

An integration test should reload from disk rather than compare the original in-memory list to itself. Change or discard that original state before checking restoration where appropriate.

from pathlib import Path
from tempfile import TemporaryDirectory
import unittest

class FileTests(unittest.TestCase):
    def setUp(self):
        self.temporary = TemporaryDirectory()
        self.addCleanup(self.temporary.cleanup)
        self.path = Path(self.temporary.name) / "note.txt"

    def test_round_trip(self):
        self.path.write_text("café", encoding="utf-8")
        self.assertEqual(self.path.read_text(encoding="utf-8"), "café")

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

07 | TEST STATE CHANGES AND NON-CHANGES

A mutating helper has more than a return value to check. For take_item in the adventure, inspect inventory and room items as well as feedback. An invalid operation should preserve the relevant earlier state.

For storage, record the original file bytes before trying to save an invalid record. If validation fails, the earlier file should remain unchanged. For a failed expense save, the application should not accept the candidate into its current list.

Tests can reveal a missing boundary even when the normal result looks correct. A function that gives the right total but changes its input unexpectedly can still break its contract.

08 | READ A TEST MATRIX

Use a table to organise inputs before writing code. The expected result comes from the stated rule, not an automatic assumption that every input should be accepted. Select a case below to inspect a model test report.

09 | A PASSING SUITE IS EVIDENCE, NOT PROOF

A suite can pass while missing an important requirement. Review whether the tests cover boundaries, rejection routes, state effects and persistence. Coverage tools can show executed lines, but execution alone does not prove that assertions are meaningful.

After fixing a bug, keep a test that would have exposed it. Rerun earlier cases after changes; this is regression testing. If a test unexpectedly fails, do not simply change its expected value to match the program. Revisit the requirement.

For unpredictable values such as today’s date, prefer passing a fixed date into the function where possible. That makes behaviour reproducible without needing every test to know the current clock.

10 | CONNECT UNIT TESTS TO THE PROJECT

The expense tracker’s test file checks amount conversion, date rules, record shape, totals, missing-file behaviour, JSON restoration and CSV quoting. Temporary directories keep those tests separate from the user’s save.

Run the suite before adding a feature. Add tests for the new requirement, implement the change, then rerun the earlier suite. A month filter should not alter the all-record total or the saved records.

Also play the whole console workflow manually: add, list, total, quit and reopen. Unit tests make checks repeatable, while a user-facing check catches unclear prompts and startup instructions.

PUT YOUR SKILLS TO WORK

Use the Section 5 challenges to practise these ideas independently. Work in your Python editor, predict results and keep a short test record.

Section 5 challenges Python course navigation

MATCH THE PYTHON TERM TO ITS MEANING

Terminology

Terminology

Test case

A specific input or situation with expected behaviour.

Expected result

The behaviour predicted independently from requirements.

Actual result

The behaviour observed when the program runs.

Unit test

A focused check of a small component.

Integration test

A check involving connected components.

System test

A check of the application as a whole.

unittest

Python’s standard automated-testing framework.

TestCase

The framework container supplying unittest assertion methods.

Assertion

A check comparing behaviour with an expectation.

assertRaises

A context-manager check that an expected exception occurs.

Fixture

Data or resources preparing a test’s starting situation.

setUp

A method run before each test to prepare its fixture.

Temporary directory

An isolated filesystem location intended for cleanup.

Boundary data

Values on or near the limits of an acceptance rule.

Invalid data

Data breaking a stated rule.

subTest

A way to label related cases within one test method.

Regression test

An earlier check rerun to detect damage from a change.

Test discovery

Finding tests according to a runner’s naming and location rules.

Questions

Questions

CHECK YOUR UNDERSTANDING

Select all correct choices. Each exact set earns one point.

1. What does a unit test mainly focus on?
2. Which methods are normally discovered as unittest tests?
3. What does assertRaises(ValueError) check?
4. Which data targets the inclusive rule zero to ten?
5. Why use temporary directories for file tests?
6. A test passes. Which conclusions are justified?
7. Which changes belong in a regression check?
8. Which assertions match absence versus position zero?

EXPLAIN AND COMPARE

Write your answers before revealing each sample.

1. Compare unit and integration tests.

2. Explain independent expected results.

3. Describe a unittest test method.

4. Explain assertRaises.

5. Design boundary cases for zero through ten.

6. Explain temporary-directory isolation.

7. Explain why checking output is not enough for mutation.

8. Evaluate a passing suite.

CODE IT | YOUR INDEPENDENT CHALLENGE

Write unittest checks for ordinary, endpoint and invalid inputs, including a temporary-file round trip.

Work in your Python editor. Keep a working version, explain one design choice and record at least two tests. Use the Learn tab for worked examples before comparing your result.

Flashcards

Flashcards

Click to flip. Select the ideas you need to revisit.

0 cards selected for revision.

    Selections are kept while this page is open.

    Workbook

    Workbook

    COMING SOON

    The workbook for Unit Tests And Test Data is coming soon. Complete the matching task and written questions in your exercise book.