AI Hints
Home›Python Tutorial›Professional Habits: Types, Tests and Logs
Python · Lesson 26 of 49

Testing Your Code

Prove it works, and keep it working.

⏱ 2 min read▶ Video at 1:20:45🎓 Free
Watch this lesson in the Python courseWatch on YouTube ↗Full Python course

Last month, Ava fixed a small bug in her discount code, and accidentally broke the delivery fee. She found out when a customer complained. Professionals protect themselves with automated tests, small programs that check the code, in seconds, every time something changes. If a test fails, you know immediately.

Testing Your Code
From the video: Testing Your Code
  • A fix in one place breaks another
  • Bugs found by angry customers
  • Automated tests find them first

Assertions

The simplest test is an assert statement. It says, this must be true. If it is, nothing happens. If not, Python raises an AssertionError. You can add a message. Here, Ava checks her discount function with a few cases. Silence means success.

asserts.pypython
def discount(price, percent):
    return round(price * (1 - percent / 100), 2)

assert discount(100, 10) == 90
assert discount(50, 0) == 50
assert discount(19.99, 50) == 10.0, "half price"
print("all assertions passed")
Error output
Traceback (most recent call last):
  File "/privateasserts.py", line 6, in <module>
    assert discount(19.99, 50) == 10.0, "half price"
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^
AssertionError: half price

unittest: the standard framework

For real projects, use a test framework. Python includes unittest. A test class has methods starting with test, and uses checks like assertEqual. Running it prints a dot for every passing test, and details for every failure. The same tests run again after every change, and protect you for years.

test_discount.pypython
import unittest

def discount(price, percent):
    return round(price * (1 - percent / 100), 2)

class TestDiscount(unittest.TestCase):
    def test_ten_percent(self):
        self.assertEqual(discount(100, 10), 90)

    def test_zero(self):
        self.assertEqual(discount(50, 0), 50)

if __name__ == "__main__":
    unittest.main(verbosity=2)
Output
test_ten_percent (__main__.TestDiscount.test_ten_percent) ... ok
test_zero (__main__.TestDiscount.test_zero) ... ok

----------------------------------------------------------------------
Ran 2 tests in 0.000s

OK

Testing errors and a failure

You can also test that errors are raised, with assertRaises. And when a test fails, the output shows exactly which one, and the expected and actual values. Here we deliberately break one test, to see the report. A failing test is not bad news, it is the system working.

test_fee.pypython
import unittest

def fee(total):
    if total < 0:
        raise ValueError("negative")
    return 0 if total >= 50 else 5

class T(unittest.TestCase):
    def test_free(self):
        self.assertEqual(fee(60), 0)
    def test_error(self):
        with self.assertRaises(ValueError):
            fee(-1)
    def test_wrong(self):
        self.assertEqual(fee(10), 0)

unittest.main(argv=["x"], exit=False)
Error output
..F
======================================================================
FAIL: test_wrong (__main__.T.test_wrong)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/privatetest_fee.py", line 15, in test_wrong
    self.assertEqual(fee(10), 0)
    ~~~~~~~~~~~~~~~~^^^^^^^^^^^^
AssertionError: 5 != 0

----------------------------------------------------------------------
Ran 3 tests in 0.001s

FAILED (failures=1)

pytest and good testing habits

Most professionals use pytest, a third party tool. Tests are simple functions with plain assert statements, and the output is beautifully clear. Whatever the tool, the habits are the same. Test one thing per test. Name tests by behaviour. Test edge cases, like zero, empty, and negative. Run tests often, and automatically, on every change.

pytestplain asserts, rich reports
One thing per testsmall and focused
Test the edgeszero, empty, negative, huge
Run on every changefast feedback, fewer bugs

Write your own tests

Your turn. Here is a function that doubles a number. Write three assert statements that check it, with a positive number, zero, and a negative number. Ready? Here is one solution. Three simple checks, and a final message when they all pass.

✍️ Exercise: Write your own tests

Write 3 asserts for double(x): a positive number, zero, and a negative number.

Show hint

assert double(4) == 8

Show solution
my_tests.pypython
def double(x):
    return x * 2

assert double(4) == 8
assert double(0) == 0
assert double(-3) == -6
print("tests passed")
Output
tests passed

Summary

Great job. You can now protect your code with assertions and unittest, check for errors, and read test failures. Pytest builds on the same ideas. Next, what do you do when something is wrong, and you do not know why? You debug, and you log.

Key takeaways
  • assert for quick checks
  • unittest classes with test_ methods
  • Test edge cases, run often
Next up: Debugging and Logging — find bugs, and leave a trail