Skip to content

Expert · Algorithms, data structures and testing · topic 44 of 50 · 30 min

Testing your code

Explanation

A test is a small piece of code that runs your code with known inputs and checks the answer. Tests catch mistakes early and tell you at once if a later change breaks something that used to work. Python has assert, the built-in unittest module, and the popular pytest tool for writing and running tests.

Why test at all?

Checking your program by hand once is not enough. Every time you change the code, something that worked yesterday may break. A bug that comes back after it was fixed, or a feature that breaks because of a change somewhere else, is called a regression. Automatic tests re-check everything in a second, every time.

Think of it like this: Think of a pressure cooker factory. Before any cooker leaves, a machine checks the whistle, the safety valve and the lid seal. Nobody trusts "it looked fine yesterday". Tests are that checking machine for your code: the same checks, run every time, before anything reaches customers.

assert with a message

assert condition, "message" does nothing when the condition is True. When it is False, Python raises an AssertionError with your message. A good message says what was expected, so the failure explains itself.

python
def add_gst(price):
    return round(price * 1.18, 2)

assert add_gst(100) == 118, "100 + 18% GST should be 118"
assert add_gst(0) == 0, "no price, no tax"

try:
    assert add_gst(50) == 60, "50 + 18% GST should be 59"
except AssertionError as err:
    print("Test failed:", err)
The first two checks pass silently. The third has a wrong expected value, so its message is shown.

Test functions

Group checks into functions whose names start with test_. Each test checks one behaviour. A common shape is Arrange, Act, Assert: prepare the input, call the code, check the result.

python
def apply_discount(price, percent):
    if not 0 <= percent <= 100:
        raise ValueError("percent must be 0 to 100")
    return price - price * percent / 100

def test_normal_discount():
    assert apply_discount(200, 10) == 180

def test_zero_discount():
    assert apply_discount(200, 0) == 200

def test_bad_percent():
    try:
        apply_discount(200, 150)
    except ValueError:
        return                       # expected: test passes
    assert False, "150% should raise ValueError"

for test in [test_normal_discount, test_zero_discount, test_bad_percent]:
    test()
print("all tests passed")
Three tiny tests, each about one behaviour, including an error case.

The unittest module

unittest comes with Python. You write a class that inherits from unittest.TestCase. Every method whose name starts with test is one test. Instead of assert you use helper methods such as assertEqual(a, b) and assertRaises(ErrorType). A method called setUp runs before every test, so each test gets fresh data.

python
import unittest

def cart_total(items):
    return sum(price * qty for price, qty in items)

class TestCartTotal(unittest.TestCase):
    def setUp(self):
        self.items = [(40, 2), (15, 4)]       # fresh for every test

    def test_total(self):
        self.assertEqual(cart_total(self.items), 140)

    def test_empty_cart(self):
        self.assertEqual(cart_total([]), 0)

    def test_bad_item(self):
        with self.assertRaises(TypeError):
            cart_total([("forty", None)])

unittest.main(argv=[""], exit=False)
unittest.main(argv=[""], exit=False) runs the tests here without closing the program. In a normal file you just call unittest.main().

pytest: the tool most teams use

pytest is a free third-party tool. It finds files named test_*.py, runs every function whose name starts with test_, and lets you use plain assert. When an assert fails, it shows the actual and expected values clearly. It also runs unittest classes, so you can mix both styles.

bash
pip install pytest
pytest            # run every test in the project
pytest -q         # quieter output
pytest -k gst     # only tests whose names contain "gst"

# Sample output:
# ..F
# FAILED test_billing.py::test_gst_on_50 - assert 59.0 == 60
Run these in a terminal, in your project folder.

An edge case is an unusual input at the border of what your code accepts. Most bugs hide there, so test them on purpose.

  • Empty input: an empty list, an empty string, no items in the cart.
  • Zero and negative numbers: 0 rupees, -5 quantity.
  • Boundaries: exactly the limit (marks of 35 when 35 is the pass mark), one below, one above.
  • One item, many items, duplicates.
  • Wrong type or bad format: "abc" where a number is expected.
Tip: Try test-driven thinking: before writing a function, write two or three tests that describe what it must do. Run them and watch them fail (red), write just enough code to pass (green), then tidy the code (refactor) while the tests guard you.
Careful: Use assert in tests, not to check user input in real programs. Python can be started with the -O option, which removes every assert, and then those checks silently disappear. In real code, raise ValueError or another proper exception.
unittestpytest
Comes with PythonYesNo, install with pip
How you checkself.assertEqual, self.assertRaisesPlain assert, pytest.raises
Test shapeMethods in a TestCase classPlain functions (classes optional)
Setup before testssetUp methodFixtures (functions that provide data)

Watch the lesson

Testing your code · 1/7

Testing your code

In this lesson you will learn to write tests: small programs that check your program.

Prefer a video by a teacher? Find free videos on “python unit testing unittest pytest tutorial for beginners” on YouTube ↗

In real life: Testing the fare calculator of a bus booking app

A bus booking app gives seniors 40% off and children under 5 a free ticket. One day a developer changes the discount code and children start paying full fare. With tests for each rule, including the edge case of a child exactly 5 years old, the mistake shows up on the developer's own laptop in seconds, long before any parent is charged wrongly.

python
def fare(base, age):
    if age < 5:
        return 0
    if age >= 60:
        return round(base * 0.6, 2)
    return base

def test_fares():
    assert fare(500, 3) == 0, "children under 5 travel free"
    assert fare(500, 5) == 500, "a 5-year-old pays full fare (edge case)"
    assert fare(500, 60) == 300, "seniors from 60 get 40% off"
    assert fare(500, 30) == 500, "adults pay the base fare"

test_fares()
print("fare tests passed")
One test function guarding every fare rule, including the borders.

Practice (0/3 done)

Task 1 · Code · Python

add_gst(price) is given. Write a test function test_add_gst() that uses assert to check at least two things: add_gst(100) == 118 and add_gst(0) == 0. The checker runs your test on the correct add_gst (it must pass) and on broken versions (it must fail).

Task 2 · Code · Python

order_total(items) is given: it adds price * qty for each (price, qty) pair and raises ValueError if any qty is negative. Complete the unittest class TestOrderTotal so it has at least 3 tests: a normal order, an empty order (total 0) and a negative quantity (use self.assertRaises(ValueError)). Your tests must pass on the correct code and catch broken versions.

Task 3 · Practice

On your computer, install pytest (pip install pytest). Create billing.py with a function and test_billing.py with at least four test functions, including two edge cases. Run pytest, then break the function on purpose and watch a test fail.

Quick quiz

Question 1

1.What does assert total == 100, "total should be 100" do when total is 90?

Question 2

2.In unittest, what is setUp used for?

Question 3

3.Which is an edge case for a function that checks if marks pass (pass mark 35)?

Question 4

4.How does pytest find your tests?

Question 5

5.Why should real programs not use assert to validate user input?

Read more (free)

Pass the quiz (60% or more) or mark this topic as done when you are ready.

Your progress is saved on this device only.

← Previous: Type hints