Introduction to Testing

Daniel Sarney
Python Intermediate

Testing is essential for building reliable software. It helps you catch bugs early, ensures code works as expected, and makes refactoring safer. Python's unittest framework provides tools for writing and running tests systematically.

In this lesson, you'll learn why testing is important, how to write unit tests, use assertions to verify behavior, organize tests, and understand test-driven development basics. These skills are crucial for professional Python development and building maintainable applications.

What You'll Learn

  • Why testing is important
  • Writing unit tests with unittest
  • Test cases and assertions
  • Running tests
  • Test-driven development basics
  • Best practices for testing

Why Testing Matters

Tests verify that your code works correctly and help prevent regressions:

# Without tests, you might not catch this bug
def divide(a, b):
    return a / b  # What if b is 0?

# With tests, you'd catch it immediately

Tests provide confidence when making changes and serve as documentation of how code should work.

Writing Your First Test

Here's a simple test using unittest:

import unittest

def add(a, b):
    """Simple addition function."""
    return a + b

class TestAdd(unittest.TestCase):
    def test_add_positive_numbers(self):
        """Test adding two positive numbers."""
        result = add(2, 3)
        self.assertEqual(result, 5)

    def test_add_negative_numbers(self):
        """Test adding negative numbers."""
        result = add(-2, -3)
        self.assertEqual(result, -5)

    def test_add_zero(self):
        """Test adding zero."""
        result = add(5, 0)
        self.assertEqual(result, 5)

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

Run tests with: python test_file.py

Common Assertions

unittest provides many assertion methods:

import unittest

class TestAssertions(unittest.TestCase):
    def test_equality(self):
        self.assertEqual(2 + 2, 4)
        self.assertNotEqual(2 + 2, 5)

    def test_truthiness(self):
        self.assertTrue(True)
        self.assertFalse(False)
        self.assertIsNone(None)
        self.assertIsNotNone("value")

    def test_containment(self):
        self.assertIn(2, [1, 2, 3])
        self.assertNotIn(4, [1, 2, 3])

    def test_types(self):
        self.assertIsInstance(5, int)
        self.assertIsInstance("hello", str)

    def test_exceptions(self):
        with self.assertRaises(ValueError):
            int("not a number")

    def test_almost_equal(self):
        # For floating point comparisons
        self.assertAlmostEqual(0.1 + 0.2, 0.3, places=7)

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

Testing Functions

Test functions with various inputs:

import unittest

def calculate_average(numbers):
    """Calculate average of numbers."""
    if not numbers:
        raise ValueError("Cannot calculate average of empty list")
    return sum(numbers) / len(numbers)

class TestCalculateAverage(unittest.TestCase):
    def test_normal_case(self):
        result = calculate_average([1, 2, 3, 4, 5])
        self.assertEqual(result, 3.0)

    def test_single_number(self):
        result = calculate_average([5])
        self.assertEqual(result, 5.0)

    def test_negative_numbers(self):
        result = calculate_average([-1, -2, -3])
        self.assertEqual(result, -2.0)

    def test_empty_list(self):
        with self.assertRaises(ValueError):
            calculate_average([])

    def test_floating_point(self):
        result = calculate_average([1.5, 2.5, 3.5])
        self.assertAlmostEqual(result, 2.5)

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

Testing Classes

Test classes and their methods:

import unittest

class BankAccount:
    def __init__(self, balance=0):
        if balance < 0:
            raise ValueError("Balance cannot be negative")
        self.balance = balance

    def deposit(self, amount):
        if amount < 0:
            raise ValueError("Deposit amount must be positive")
        self.balance += amount

    def withdraw(self, amount):
        if amount < 0:
            raise ValueError("Withdrawal amount must be positive")
        if amount > self.balance:
            raise ValueError("Insufficient funds")
        self.balance -= amount

    def get_balance(self):
        return self.balance

class TestBankAccount(unittest.TestCase):
    def setUp(self):
        """Set up test fixtures before each test."""
        self.account = BankAccount(100)

    def test_initial_balance(self):
        account = BankAccount(50)
        self.assertEqual(account.get_balance(), 50)

    def test_negative_initial_balance(self):
        with self.assertRaises(ValueError):
            BankAccount(-10)

    def test_deposit(self):
        self.account.deposit(50)
        self.assertEqual(self.account.get_balance(), 150)

    def test_withdraw(self):
        self.account.withdraw(30)
        self.assertEqual(self.account.get_balance(), 70)

    def test_insufficient_funds(self):
        with self.assertRaises(ValueError):
            self.account.withdraw(200)

    def tearDown(self):
        """Clean up after each test."""
        pass  # Usually not needed for simple tests

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

setUp and tearDown

Use setUp() and tearDown() for test preparation and cleanup:

import unittest
import os
import tempfile

class TestFileOperations(unittest.TestCase):
    def setUp(self):
        """Create temporary file before each test."""
        self.temp_file = tempfile.NamedTemporaryFile(delete=False, mode='w')
        self.temp_file.write("test content")
        self.temp_file.close()
        self.temp_path = self.temp_file.name

    def tearDown(self):
        """Clean up temporary file after each test."""
        if os.path.exists(self.temp_path):
            os.unlink(self.temp_path)

    def test_file_exists(self):
        self.assertTrue(os.path.exists(self.temp_path))

    def test_file_content(self):
        with open(self.temp_path, 'r') as f:
            content = f.read()
        self.assertEqual(content, "test content")

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

Test Organization

Organize tests in separate files:

# calculator.py
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

# test_calculator.py
import unittest
from calculator import Calculator

class TestCalculator(unittest.TestCase):
    def setUp(self):
        self.calc = Calculator()

    def test_add(self):
        self.assertEqual(self.calc.add(2, 3), 5)

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

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

    def test_divide(self):
        self.assertEqual(self.calc.divide(10, 2), 5)

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

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

Run tests: python -m unittest test_calculator.py

Test-Driven Development (TDD)

TDD means writing tests before code:

# Step 1: Write a failing test
import unittest

class TestFactorial(unittest.TestCase):
    def test_factorial(self):
        self.assertEqual(factorial(5), 120)  # Function doesn't exist yet!

# Step 2: Write minimal code to pass
def factorial(n):
    if n <= 1:
        return 1
    return n * factorial(n - 1)

# Step 3: Refactor if needed
# Step 4: Add more tests
class TestFactorial(unittest.TestCase):
    def test_factorial_zero(self):
        self.assertEqual(factorial(0), 1)

    def test_factorial_one(self):
        self.assertEqual(factorial(1), 1)

    def test_factorial_negative(self):
        with self.assertRaises(ValueError):
            factorial(-1)

Practical Testing Examples

Here are comprehensive testing examples:

# Example 1: Testing string utilities
import unittest

def reverse_string(text):
    """Reverse a string."""
    return text[::-1]

def is_palindrome(text):
    """Check if text is a palindrome."""
    text = text.lower().replace(" ", "")
    return text == text[::-1]

class TestStringUtils(unittest.TestCase):
    def test_reverse_string(self):
        self.assertEqual(reverse_string("hello"), "olleh")
        self.assertEqual(reverse_string(""), "")

    def test_is_palindrome(self):
        self.assertTrue(is_palindrome("racecar"))
        self.assertTrue(is_palindrome("A man a plan a canal Panama"))
        self.assertFalse(is_palindrome("hello"))

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

# Example 2: Testing data processing
import unittest

def process_scores(scores):
    """Process and validate scores."""
    if not scores:
        raise ValueError("Scores list cannot be empty")
    valid_scores = [s for s in scores if 0 <= s <= 100]
    if not valid_scores:
        raise ValueError("No valid scores found")
    return {
        "average": sum(valid_scores) / len(valid_scores),
        "max": max(valid_scores),
        "min": min(valid_scores),
        "count": len(valid_scores)
    }

class TestProcessScores(unittest.TestCase):
    def test_normal_scores(self):
        result = process_scores([85, 90, 78, 92])
        self.assertEqual(result["average"], 86.25)
        self.assertEqual(result["max"], 92)
        self.assertEqual(result["min"], 78)

    def test_empty_list(self):
        with self.assertRaises(ValueError):
            process_scores([])

    def test_filters_invalid(self):
        result = process_scores([85, 150, 90, -10, 78])
        self.assertEqual(result["count"], 3)  # Only valid scores

Try It Yourself

Practice writing tests:

  1. Math Functions: Write tests for functions that calculate area, perimeter, and volume of shapes.

  2. Data Validation: Create tests for functions that validate email addresses, phone numbers, and passwords.

  3. List Operations: Write tests for functions that manipulate lists (sorting, filtering, transforming).

  4. Class Testing: Create comprehensive tests for a class with multiple methods and edge cases.

  5. TDD Exercise: Use TDD to implement a function that converts between different temperature scales.

Summary

Testing is essential for building reliable software. The unittest framework provides tools for writing and organizing tests. Use assertions to verify behavior, setUp() and tearDown() for preparation and cleanup, and organize tests in separate files. Test-driven development helps you write better code by thinking about requirements first.

Writing tests takes time initially but saves time in the long run by catching bugs early and enabling confident refactoring. As you continue developing, make testing a regular part of your workflow.

What's Next?

In the final lesson, we'll put everything together by building a complete intermediate-level application. You'll combine OOP, modules, advanced features, and best practices to create a real-world project that demonstrates all the concepts you've learned in this course.

Video Tutorial Coming Soon

The video tutorial for this lesson will be available soon. Check back later!

Continue Learning

12 lessons
Python For Beginners

Start your learning journey with Python For Beginners. This course includes comprehensive lessons covering everything you need to know.