Unit Testing

What is Unit Testing?

Unit testing is the process of checking individual pieces (units) of your code, such as functions, to make sure they behave correctly in isolation. Python doesn't force you to write tests, but using a tool like pytest makes testing simple and powerful.

Pytest

Pytest is a Python testing framework that automates running your tests. Install it with:

pip install pytest

Then you can write a test similar to

# In test_file.py
from file_to_test import function_to_test
def test_function_to_test():
    assert function_to_test(3) == 6

This test will pass if function_to_test(3) returns 6, and fail otherwise, raising an AssertionError. You can run your test from the terminal with:

pytest test_file.py

Pytest will search for all functions in that file starting with test_ and run them.

The assert statement checks if a condition is True. If it's not, it raises an AssertionError.

assert 2 + 2 == 4  # Passes
assert 2 + 2 == 5  # Raises AssertionError

Sometimes, you want to handle failed assertions manually with try and except keywords. This method can be useful when debugging or logging failures, though it's more common to let pytest handle assertions automatically.

def test_func():
    try:
        assert function_to_test(3) == 6
    except AssertionError:
        print("Test failed: output did not match expected value")

Errors

Errors in Python are called exceptions. They're all subclasses of the built-in class BaseException.

There are two main kinds of errors; syntax errors and runtime errors.

  • Syntax Errors: Raised before the program runs and arise from incorrectly written code.
  • Runtime Errors: Raised while the program is running.

Runtime Errors

Exception Description
SyntaxError Code is not syntactically correct
NameError Variable not defined
TypeError Invalid type operation (e.g., modifying a tuple)
IndexError List index out of range
KeyError Dictionary key doesn't exist
FileNotFoundError File does not exist
ZeroDivisionError Dividing by zero
AssertionError Assertion failed
ValueError Wrong number/type of values

Raising Your Own Errors

You can raise errors yourself with custom messages.

raise ValueError("This is a custom error message.")

Handling Exceptions

Use try/except blocks to catch and handle exceptions without crashing your program.

try:
    risky_code()
except ZeroDivisionError:
    print("You can't divide by zero!")

Full Structure: try, except, else, finally.

try:
    result = 10 / 2
except ZeroDivisionError:
    print("Cannot divide by zero.")
else:
    print("Division successful:", result)
finally:
    print("This always runs.")
  • try: Run this block
  • except: Handle an error if one happens
  • else: Runs if no exception occurred
  • finally: Always runs, useful for cleanup

You can use exception handling inside a loop for repeated attempts:

while True:
    try:
        value = int(input("Enter a number: "))
    except ValueError:
        print("That wasn't a number. Try again.")
    else:
        break

Ignoring Errors

try:
    risky_thing()
except FileNotFoundError:
    pass  # Silently ignore the error

Error Information

You can get extra information from your error using the errorName as e syntax. Here 'e' is used similar to the this keyword where it will refer to the error.

try:
    open("file.txt")
except FileNotFoundError as e:
    print(f"Error: {e.filename} not found.")