Loading...

Go Back

Next page
Go Back Course Outline

Python Full Course


Testing and Debugging in Python

Comprehensive Guide to Testing and Debugging in Python

1. Unit Testing with unittest

What is Unit Testing?

Unit testing involves testing individual components (functions, methods, classes) to ensure they work correctly. Python's built-in unittest module provides tools for:

  • Writing test cases
  • Running test suites
  • Checking assertions (expected vs. actual results)

Example: Writing Test Cases

                        # calculator.py (Module to test)
                        def add(a, b):
                            return a + b
                        
                        def subtract(a, b):
                            return a - b
                        
                        def multiply(a, b):
                            return a * b
                        
                        def divide(a, b):
                            if b == 0:
                                raise ValueError("Cannot divide by zero!")
                            return a / b
                        # test_calculator.py (Test file)
                        import unittest
                        from calculator import add, subtract, multiply, divide
                        
                        class TestCalculator(unittest.TestCase):
                            def test_add(self):
                                self.assertEqual(add(2, 3), 5)  # 2 + 3 = 5
                                self.assertEqual(add(-1, 1), 0)  # -1 + 1 = 0
                        
                            def test_subtract(self):
                                self.assertEqual(subtract(5, 3), 2)  # 5 - 3 = 2
                                self.assertEqual(subtract(10, -5), 15)  # 10 - (-5) = 15
                        
                            def test_multiply(self):
                                self.assertEqual(multiply(3, 4), 12)  # 3 * 4 = 12
                                self.assertEqual(multiply(-2, 5), -10)  # -2 * 5 = -10
                        
                            def test_divide(self):
                                self.assertEqual(divide(10, 2), 5)  # 10 / 2 = 5
                                self.assertAlmostEqual(divide(1, 3), 0.333, places=3)  # Approximate float comparison
                                
                                # Test exception handling
                                with self.assertRaises(ValueError):
                                    divide(10, 0)  # Should raise ValueError
                        
                        if __name__ == "__main__":
                            unittest.main()

Running the Tests

                        python -m unittest test_calculator.py
.... ---------------------------------------------------------------------- Ran 4 tests in 0.001s OK
Key:
- . means a test passed
- F would mean a test failed
- E would mean an error occurred

Test-Driven Development (TDD)

TDD follows this cycle:

  1. Write a failing test (before writing the actual code)
  2. Write minimal code to pass the test
  3. Refactor (improve code without breaking tests)

Example: TDD for a factorial function

                        # Step 1: Write the test first
                        def test_factorial(self):
                            self.assertEqual(factorial(0), 1)
                            self.assertEqual(factorial(5), 120)
                        # Step 2: Implement the function
                        def factorial(n):
                            if n == 0:
                                return 1
                            return n * factorial(n - 1)
                        # Step 3: Run the test
                        python -m unittest test_factorial.py


2. Debugging with pdb

What is Debugging?

Debugging is the process of finding and fixing errors in code. Python provides:

  • pdb (Python Debugger): Interactive debugging tool
  • Breakpoints: Pause execution at specific lines
  • Step-through execution: Execute code line by line

Example: Debugging with pdb

                        # buggy_script.py
                        def divide_numbers(a, b):
                            result = a / b  # Potential ZeroDivisionError
                            return result
                        
                        def main():
                            x = 10
                            y = 0
                            print(divide_numbers(x, y))
                        
                        if __name__ == "__main__":
                            import pdb; pdb.set_trace()  # Start debugger
                            main()

Running the Debugger

                        python buggy_script.py
> /path/to/buggy_script.py(10)() -> main() (Pdb)
pdb commands:
- n (next line)
- c (continue)
- s (step into function)
- l (list code)
- p <variable> (print variable)
- q (quit)

Debugging Session Example

(Pdb) n # Step to next line > /path/to/buggy_script.py(5)main() -> x = 10 (Pdb) n > /path/to/buggy_script.py(6)main() -> y = 0 (Pdb) n > /path/to/buggy_script.py(7)main() -> print(divide_numbers(x, y)) (Pdb) s # Step into divide_numbers > /path/to/buggy_script.py(2)divide_numbers() -> result = a / b (Pdb) p a, b # Print variables (10, 0) (Pdb) n # Execute division (raises error) ZeroDivisionError: division by zero

3. Logging for Debugging

What is Logging?

Logging helps track program execution by recording messages (debug, info, warnings, errors).

Example: Using logging

                        import logging
                        
                        # Configure logging
                        logging.basicConfig(
                            level=logging.DEBUG,  # Minimum level to log
                            format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
                            filename="app.log"  # Log to file
                        )
                        
                        def divide(a, b):
                            logging.debug(f"Attempting division: {a} / {b}")
                            try:
                                result = a / b
                                logging.info(f"Division successful: {result}")
                                return result
                            except ZeroDivisionError:
                                logging.error("Division by zero!")
                                raise
                        
                        divide(10, 2)
                        divide(5, 0)

Output (app.log)

2023-10-01 12:00:00 - root - DEBUG - Attempting division: 10 / 2 2023-10-01 12:00:00 - root - INFO - Division successful: 5.0 2023-10-01 12:00:00 - root - DEBUG - Attempting division: 5 / 0 2023-10-01 12:00:00 - root - ERROR - Division by zero!


4. Code Profiling

What is Profiling?

Profiling measures code performance (execution time, memory usage).

Example: Using cProfile

                        # slow_function.py
                        import time
                        
                        def slow_function():
                            total = 0
                            for i in range(1000000):
                                total += i
                            return total
                        
                        if __name__ == "__main__":
                            import cProfile
                            cProfile.run("slow_function()")

Running the Profiler

                        python slow_function.py
4 function calls in 0.100 seconds Ordered by: standard name ncalls tottime percall cumtime percall filename:lineno(function) 1 0.100 0.100 0.100 0.100 slow_function.py:4(slow_function) 1 0.000 0.000 0.100 0.100 :1() 1 0.000 0.000 0.100 0.100 {built-in method builtins.exec} 1 0.000 0.000 0.000 0.000 {method 'disable' of '_lsprof.Profiler' objects}
Key Metrics:
- ncalls: Number of calls
- tottime: Total time spent in the function
- cumtime: Cumulative time (including sub-calls)

Summary

TopicKey ToolsUse Case
Unit Testingunittest, pytestVerify individual functions
Debuggingpdb, breakpoint()Step-through code execution
Logginglogging moduleTrack runtime events
ProfilingcProfile, timeitMeasure performance

Best Practices

  • Write tests before code (TDD)
  • Use pdb for interactive debugging
  • Log important events (DEBUG, INFO, ERROR)
  • Profile before optimizing
Go Back

Next page