Web Analytics

Unit Testing

Advanced ~30 min read

Unit testing is essential for writing reliable, maintainable code. Python's built-in unittest module provides a comprehensive testing framework inspired by JUnit. Learn to write test cases, use assertions, test exceptions, and organize your tests effectively. Good tests catch bugs early and give confidence when refactoring!

Basic Test Structure

The unittest module provides the TestCase class that you inherit from. Test methods must start with test_ to be automatically discovered. Use assertion methods like assertEqual, assertTrue, and assertRaises to verify expected behavior.

Output
Click Run to execute your code
Test Method Naming:
- Methods starting with test_ are automatically discovered and run
- Use descriptive names: test_add_positive_numbers, test_divide_by_zero_raises_error
- One test method should test one specific behavior
- Keep tests simple and focused

Common Assertions

The TestCase class provides many assertion methods to verify expected behavior. Use the most specific assertion that fits your test case for clearer error messages.

Output
Click Run to execute your code
Pro Tip: Use specific assertions like assertEqual instead of assertTrue(x == y). Specific assertions provide better error messages showing the expected and actual values when tests fail!

Testing Exceptions

Use assertRaises to verify that functions raise expected exceptions under certain conditions. This ensures your error handling works correctly.

Output
Click Run to execute your code

setUp and tearDown

The setUp method runs before each test method, and tearDown runs after. Use them to initialize test fixtures (common test data) and clean up resources. This keeps test methods focused on testing logic rather than setup code.

Output
Click Run to execute your code
Important: Tests should be independent - each test should be able to run alone and in any order. Don't rely on test execution order or shared state between tests. Use setUp to create fresh test data for each test!

Common Mistakes

1. Forgetting 'test_' prefix on test methods

# Wrong - won't be discovered by unittest
class TestMath(unittest.TestCase):
    def check_add(self):  # No 'test_' prefix!
        self.assertEqual(add(2, 3), 5)

# Correct - method starts with 'test_'
class TestMath(unittest.TestCase):
    def test_add(self):  # Correct prefix
        self.assertEqual(add(2, 3), 5)

2. Testing multiple things in one test

# Wrong - multiple assertions for different behaviors
def test_math_operations(self):
    self.assertEqual(add(2, 3), 5)
    self.assertEqual(subtract(5, 2), 3)
    self.assertEqual(multiply(4, 3), 12)
    # If first fails, you don't know if others work

# Correct - one test per behavior
def test_add(self):
    self.assertEqual(add(2, 3), 5)

def test_subtract(self):
    self.assertEqual(subtract(5, 2), 3)

def test_multiply(self):
    self.assertEqual(multiply(4, 3), 12)

3. Not testing edge cases and error conditions

# Wrong - only testing happy path
def test_divide(self):
    self.assertEqual(divide(10, 2), 5)

# Correct - test normal case AND edge cases
def test_divide_normal(self):
    self.assertEqual(divide(10, 2), 5)

def test_divide_by_zero(self):
    with self.assertRaises(ValueError):
        divide(10, 0)

def test_divide_negative(self):
    self.assertEqual(divide(-10, 2), -5)

4. Tests that depend on execution order

# Wrong - tests depend on each other
class TestCounter(unittest.TestCase):
    counter = 0  # Class variable - shared state!
    
    def test_increment_first(self):
        TestCounter.counter += 1
        self.assertEqual(TestCounter.counter, 1)
    
    def test_increment_second(self):
        TestCounter.counter += 1
        self.assertEqual(TestCounter.counter, 2)  # Fails if run alone!

# Correct - each test is independent
class TestCounter(unittest.TestCase):
    def setUp(self):
        self.counter = 0  # Fresh instance variable per test
    
    def test_increment(self):
        self.counter += 1
        self.assertEqual(self.counter, 1)

Exercise: Write Comprehensive Tests

Task: Write a complete test suite for a Calculator class that has methods for basic math operations. Test both normal cases and edge cases.

Requirements:

  • Create a TestCalculator class inheriting from unittest.TestCase
  • Use setUp to create a Calculator instance
  • Test add method: positive numbers, negative numbers, zero
  • Test divide method: normal division and division by zero (should raise ValueError)
  • Test multiply method: normal case, with zero, negative numbers
  • Run the tests and verify they all pass
Output
Click Run to execute your code
Show Solution
import unittest

class Calculator:
    def add(self, a, b):
        return a + b
    
    def subtract(self, a, b):
        return a - b
    
    def multiply(self, a, b):
        return a * b
    
    def divide(self, a, b):
        if b == 0:
            raise ValueError("Cannot divide by zero")
        return a / b


class TestCalculator(unittest.TestCase):
    def setUp(self):
        """Set up test fixtures before each test method."""
        self.calc = Calculator()
    
    def test_add_positive(self):
        self.assertEqual(self.calc.add(2, 3), 5)
    
    def test_add_negative(self):
        self.assertEqual(self.calc.add(-2, -3), -5)
    
    def test_add_zero(self):
        self.assertEqual(self.calc.add(5, 0), 5)
    
    def test_divide_normal(self):
        self.assertEqual(self.calc.divide(10, 2), 5.0)
    
    def test_divide_by_zero(self):
        with self.assertRaises(ValueError):
            self.calc.divide(10, 0)
    
    def test_multiply_normal(self):
        self.assertEqual(self.calc.multiply(3, 4), 12)
    
    def test_multiply_with_zero(self):
        self.assertEqual(self.calc.multiply(5, 0), 0)
    
    def test_multiply_negative(self):
        self.assertEqual(self.calc.multiply(-2, 3), -6)


if __name__ == '__main__':
    unittest.main()

Summary

  • unittest Module: Python's built-in testing framework, inspired by JUnit
  • TestCase: Inherit from unittest.TestCase to create test classes
  • Test Methods: Methods starting with test_ are automatically discovered and run
  • Assertions: Use assertEqual, assertTrue, assertRaises, etc. for verification
  • setUp/tearDown: Run before/after each test method for fixture setup and cleanup
  • Testing Exceptions: Use assertRaises to verify functions raise expected exceptions
  • Test Independence: Tests should be independent and runnable in any order
  • Best Practices: One test per behavior, test edge cases, use descriptive names, keep tests simple

What's Next?

Unit testing ensures your code works correctly! Next, we'll explore logging, which helps you track application behavior and debug issues in production. The logging module is much more powerful than print() statements and is essential for professional Python applications!