Write Python Unit Tests Around Observable Behaviour

A useful test should explain what a caller is entitled to observe. This workshop tests a fictional delivery charge with a threshold of twenty. It does not process orders or money. You will derive expected answers independently, check the threshold and its neighbours, and diagnose a deliberately wrong comparison without inspecting how many temporary variables the function uses.

Documentation boundary

Python’s unittest documentation describes TestCase assertions, including assertEqual for results and assertRaises for expected exceptions. A test is not an error simply because it expects an exception: the assertion checks that the specified exceptional behaviour occurs.

Derive the expected results

The exercise contract charges three units below a total of twenty and zero units at or above twenty. Negative totals are rejected. These are invented units, not a real shop policy. Before writing the test code, calculate the outcomes for zero, nineteen, twenty and twenty-one. Add a separate rejected case for minus one. The threshold itself matters because a wrong comparison can pass examples far from the boundary.

Fictional total Expected outcome Reason
0 3 Below threshold
19 3 Nearest lower whole number
20 0 Threshold included
21 0 Above threshold
-1 ValueError Rejected range

Do not compute expected values by calling the function under test a second time. That would compare the implementation with itself. The table is the independent statement of the contract. Your task is to translate each row into an assertion and ensure the negative case cannot be mistaken for a valid free delivery.

Run the worked tests

import unittest
import io

def delivery_charge(total):
    if total < 0:
        raise ValueError("negative order")
    return 0 if total >= 20 else 3

class ChargeTests(unittest.TestCase):
    def test_threshold_and_neighbours(self):
        for total, expected in [(0, 3), (19, 3), (20, 0), (21, 0)]:
            with self.subTest(total=total):
                self.assertEqual(delivery_charge(total), expected)
    def test_negative_rejected(self):
        with self.assertRaises(ValueError):
            delivery_charge(-1)

suite = unittest.defaultTestLoader.loadTestsFromTestCase(ChargeTests)
result = unittest.TextTestRunner(stream=io.StringIO()).run(suite)
assert result.wasSuccessful()
assert result.testsRun == 2
print("two test methods passed; four boundary subcases checked")

The result contains two test methods. The first checks four subcases and the second checks the rejected range. A report of two methods does not mean that only two inputs were considered, so describe the coverage precisely. The example captures the runner’s report in memory; the webpage does not execute the tests or provide a live testing service.

Now make a scratch change from greater-than-or-equal to strictly greater-than at the threshold. The twenty-unit case should fail while the other ordinary cases remain consistent. This is a useful diagnostic variation because it targets a meaningful rule. Restore the correct comparison after recording which case exposed the mistake. Preserve that observation as evidence that the tests distinguish a plausible defect.

Avoid tests that mirror the implementation

A test that asserts the name of an internal variable, the order of two harmless calculations or the number of source lines is not checking this caller contract. A rewritten function could preserve every promised outcome while changing those details. Prefer assertions about the returned charge and rejected inputs. Implementation-specific checks may serve another purpose, but they need a reason beyond matching the current code.

Likewise, one large assertion about a printed report can make a failure hard to locate. The boundary table gives each case a clear meaning. If the result for nineteen fails, inspect the below-threshold rule. If only twenty fails, inspect inclusivity. If minus one returns zero, inspect the rejected-range boundary. A useful failure should point toward a violated requirement, not merely say that a text blob changed.

Assess the limits honestly

These tests do not prove every possible input is supported. The exercise contract uses whole-number fictional totals, and it does not define currency rounding, tax, discounts or string conversion. Do not add a claim that those topics have been covered. For an extension, define one additional requirement, such as rejecting non-numeric input, then add an independently predicted example before altering the implementation.

Submit the contract table, the two test methods, the passing result and the deliberately wrong threshold experiment. State which defect the tests detected and which behaviours remain outside scope. That final distinction is part of test quality: a precise small guarantee is more useful than an unsupported statement that the function works for everything.

Questions for this exercise

Can a passing test expect an exception?

Yes. assertRaises passes when the specified exception occurs under the checked call; it is a test of expected behaviour.

Do two test methods mean two input cases?

No. Here one method checks four boundary subcases and another checks one rejected-range case.

What does the threshold experiment demonstrate?

The twenty-unit case detects the deliberate strict-comparison defect; it does not establish every possible requirement.

Official documentation

Explore the Courses overview or use Contact for general enquiries. This is an original INTIAU editorial self-study exercise; no qualification is awarded.

Related guides

View the study diagram
Four cards define a fictional charge boundary, check neighbouring values, reject a negative input and identify a deliberately wrong comparison.
Original explanatory diagram; consult the linked official guidance for current requirements.

Privacy choices

Optional analytics, advertising measurement and personalisation, and hosting telemetry stay off until you accept. Essential sign-in, security and application tools remain available. You can change this choice at any time.

Read our privacy policy