Unit Testing
Learn why automated testing matters and how to write and run tests in Python using the built-in unittest module and the popular pytest library.
Introduction
So far, you have checked whether your code works by running it and looking at the output yourself. That works fine for a five-line script, but it quickly breaks down as programs grow — you cannot realistically re-check every function by hand every time you change a single line.
Unit testing solves this by letting you write code that checks your code. A test is simply a small piece of Python that calls a function with known inputs and verifies it produces the expected output — automatically, repeatedly, and in seconds. This is the final lesson of the course, and it ties everything you have learned together: you will use functions, classes, exceptions, and modules to build a safety net around the programs you write.
- Why automated tests matter for real projects.
- How to write tests with the built-in unittest module.
- Common assertion methods like assertEqual and assertRaises.
- How to run your test suite.
- How pytest offers a simpler, modern alternative.
Why Automated Testing Matters
As a program grows, changing one function can accidentally break another part of the code without you noticing — this is called a regression. Automated tests catch regressions immediately, because you can re-run every test in seconds after any change.
Catches Regressions
Tests immediately flag when a change breaks existing behavior elsewhere in the program.
Confidence to Refactor
You can improve or rewrite code freely, knowing tests will catch anything you break.
Living Documentation
Tests show exactly how a function is expected to be used, with real examples.
Fast Feedback
Running a full test suite takes seconds, far faster than manually checking everything by hand.
Team Safety Net
Tests let multiple developers change shared code without constantly breaking each other's work.
The unittest Module
Python ships with a built-in testing framework called unittest. To use it, you create a class that inherits from unittest.TestCase, and write methods whose names start with test_.
import unittest
class TestMath(unittest.TestCase):
def test_addition(self):
self.assertEqual(2 + 2, 4)
def test_subtraction(self):
self.assertEqual(5 - 3, 2)
if __name__ == "__main__":
unittest.main()..
----------------------------------------------------------------
Ran 2 tests in 0.001s
OKunittest automatically discovers every method starting with test_ inside a TestCase subclass and runs each one independently. Each dot in the output represents one passing test.
Common Assertions
Assertions are the checks inside a test method. If an assertion fails, unittest marks that test as failed and reports what went wrong.
assertEqual(a, b)
Passes if a and b are equal. The most commonly used assertion.
assertNotEqual(a, b)
Passes if a and b are not equal.
assertTrue(x)
Passes if x evaluates to True.
assertFalse(x)
Passes if x evaluates to False.
assertIsNone(x)
Passes if x is None.
assertRaises(Error)
Passes if the given code block raises the specified exception.
import unittest
class TestAssertions(unittest.TestCase):
def test_equal(self):
self.assertEqual("python".upper(), "PYTHON")
def test_true(self):
self.assertTrue(5 > 3)
def test_in_list(self):
self.assertTrue("apple" in ["apple", "banana"])
if __name__ == "__main__":
unittest.main()...
----------------------------------------------------------------
Ran 3 tests in 0.001s
OKRunning Your Tests
You can run a test file directly from the command line, either by executing the file itself (if it calls unittest.main()) or using Python's built-in test discovery.
python test_basic.py
# or, to discover and run every test file automatically:
python -m unittest discover..
----------------------------------------------------------------
Ran 2 tests in 0.001s
OKWhen a test fails, unittest shows exactly which assertion failed, the expected value, and the actual value, making it easy to pinpoint the problem.
import unittest
class TestMath(unittest.TestCase):
def test_addition(self):
self.assertEqual(2 + 2, 5) # deliberately wrong
if __name__ == "__main__":
unittest.main()F
======================================================================
FAIL: test_addition (__main__.TestMath)
----------------------------------------------------------------
AssertionError: 4 != 5
----------------------------------------------------------------
Ran 1 test in 0.001s
FAILED (failures=1)Testing for Exceptions
Sometimes correct behavior means raising an error — for example, dividing by zero should fail loudly rather than silently returning a wrong number. assertRaises checks that the expected exception actually occurs.
import unittest
def divide(a, b):
if b == 0:
raise ValueError("Cannot divide by zero")
return a / b
class TestDivide(unittest.TestCase):
def test_normal_division(self):
self.assertEqual(divide(10, 2), 5)
def test_divide_by_zero_raises(self):
with self.assertRaises(ValueError):
divide(10, 0)
if __name__ == "__main__":
unittest.main()..
----------------------------------------------------------------
Ran 2 tests in 0.001s
OKA Worked Example
Let's test a small, realistic function: one that calculates a discounted price. We will cover a normal case, an edge case, and an invalid input case.
def calculate_discounted_price(price, discount_percent):
if price < 0:
raise ValueError("Price cannot be negative")
if not (0 <= discount_percent <= 100):
raise ValueError("Discount must be between 0 and 100")
return price - (price * discount_percent / 100)import unittest
from shopping import calculate_discounted_price
class TestDiscountedPrice(unittest.TestCase):
def test_normal_discount(self):
self.assertEqual(calculate_discounted_price(200, 10), 180)
def test_zero_discount(self):
self.assertEqual(calculate_discounted_price(150, 0), 150)
def test_full_discount(self):
self.assertEqual(calculate_discounted_price(150, 100), 0)
def test_negative_price_raises(self):
with self.assertRaises(ValueError):
calculate_discounted_price(-50, 10)
def test_invalid_discount_raises(self):
with self.assertRaises(ValueError):
calculate_discounted_price(100, 150)
if __name__ == "__main__":
unittest.main().....
----------------------------------------------------------------
Ran 5 tests in 0.001s
OKNotice how each test covers one specific behavior. This makes it immediately obvious which part of calculate_discounted_price broke if a test ever fails after a future change.
pytest: A Simpler Alternative
While unittest ships with Python and works well, many developers prefer pytest, a hugely popular third-party testing library. pytest lets you write tests as plain functions using ordinary assert statements — no test class or self.assertEqual boilerplate required.
pip install pytestfrom shopping import calculate_discounted_price
import pytest
def test_normal_discount():
assert calculate_discounted_price(200, 10) == 180
def test_zero_discount():
assert calculate_discounted_price(150, 0) == 150
def test_negative_price_raises():
with pytest.raises(ValueError):
calculate_discounted_price(-50, 10)pytest test_shopping_pytest.py===================== test session starts ======================
collected 3 items
test_shopping_pytest.py ... [100%]
====================== 3 passed in 0.02s =======================unittest
- Built into Python — no installation needed.
- Tests live inside a TestCase class.
- Uses self.assertEqual(), self.assertTrue(), etc.
- Slightly more boilerplate per test file.
pytest
- Popular third-party library — install with pip.
- Tests can be plain functions.
- Uses ordinary Python assert statements.
- Detailed, readable failure output out of the box.
Common Mistakes
- Naming test methods without the test_ prefix — unittest and pytest will silently skip them.
- Writing one giant test that checks many unrelated things, making failures hard to diagnose.
- Testing only the "happy path" and forgetting edge cases like empty input, zero, or negative numbers.
- Not testing error cases — a function that should raise an exception needs a test confirming it actually does.
- Letting tests depend on each other's order or shared mutable state.
Best Practices
- Give each test a descriptive name that explains what it checks.
- Keep each test focused on one specific behavior.
- Cover normal cases, edge cases, and error cases.
- Run your full test suite before and after making changes.
- Keep tests fast so you actually run them often.
Frequently Asked Questions
Do I need to test every single function I write?
Not necessarily every one, but any function with real logic, edge cases, or error handling benefits greatly from tests, especially code other parts of your program depend on.
Should I use unittest or pytest for a new project?
unittest is great to learn first since it is built into Python. Most real-world Python teams prefer pytest for its simpler syntax and better failure messages, so it is worth learning both.
What is the difference between a unit test and just running my program?
Running your program checks it works right now, for the inputs you happened to try. A unit test is saved, automated, and can be re-run instantly against many inputs every time the code changes.
Where should test files live in a project?
A common convention is a separate tests/ folder, or files named test_<module>.py alongside the code they test. Both unittest discovery and pytest can automatically find files following this pattern.
What happens if a test fails?
The test runner reports which test failed, along with the expected and actual values, so you can quickly locate and fix the bug without manually re-checking the entire program.
Key Takeaways
- Automated tests catch regressions and give you confidence to refactor code safely.
- unittest is Python's built-in testing framework, based on TestCase classes and test_ methods.
- Common assertions include assertEqual, assertTrue, and assertRaises.
- pytest is a popular alternative that uses plain functions and ordinary assert statements.
- Good tests cover normal cases, edge cases, and expected errors.
Summary
Unit testing turns "I think my code works" into "I can prove my code works, automatically, every time." Whether you use the built-in unittest module or the simpler pytest library, writing tests alongside your code is one of the most valuable habits you can build as a Python developer.
- You understand why automated testing matters for real projects.
- You can write tests using unittest.TestCase and test_ methods.
- You know the common assertions: assertEqual, assertTrue, and assertRaises.
- You have seen how pytest simplifies testing with plain assert statements.
- You have completed every lesson in the Python course!
What to Learn Next
Congratulations — you have reached the end of this 58-lesson Python course! You now have a solid foundation covering syntax, data structures, functions, object-oriented programming, error handling, modules, type hints, context managers, and testing. That foundation is exactly what you need to specialize in whatever area excites you most.
Web Development
Learn Django or Flask to build full websites and APIs using the Python skills you already have.
Data Science
Explore pandas, NumPy, and Matplotlib to clean, analyze, and visualize real-world data.
Machine Learning
Dive into scikit-learn, TensorFlow, or PyTorch to build predictive models and AI systems.
Automation & Scripting
Use your Python skills to automate repetitive tasks, working with files, spreadsheets, and the web.
Build Real Projects
The single best way to solidify everything you have learned is to build small, complete projects of your own.
Whichever path you choose, keep writing code, keep testing it, and keep building. That is how every Python developer, no matter how experienced, continues to grow.