Skip to content

How to Validate Method Behaviors with Testing in Python

Learn how to use Python's unittest framework, assertions, and mocking to validate method return values, exception handling, and dependency interactions for reliable code.

Validating method behaviors ensures that your code performs as expected under various conditions, handles errors gracefully, and maintains stability during refactoring. Without proper validation, subtle bugs can propagate through a system, leading to unexpected crashes or incorrect data processing.

This guide explores how to validate method behaviors using Python's built-in unittest framework, assertion logic, and modern testing practices like mocking dependencies.

Understanding Method Validation

Method validation goes beyond checking if a function runs without crashing. It involves three key pillars:

  • Return Value Verification: Does the method return the correct output for a given input?
  • State Change Verification: If the method modifies an object (e.g., updates a list or database), is the state correctly updated?
  • Behavioral Verification: Does the method call other internal methods or external services correctly?

Method 1: Using the unittest Framework

Python's standard library includes unittest, a robust framework for creating test suites. To validate a method, you create a class inheriting from unittest.TestCase and use various assert methods.

Validating Return Values

Suppose we have a simple calculator class.

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

And we want to test it using unittest framework:

import unittest

class TestCalculator(unittest.TestCase):
    def setUp(self):
        """Runs before every test method."""
        self.calc = Calculator()

    def test_add_method_returns_sum(self):
        result = self.calc.add(10, 5)
        self.assertEqual(result, 15)
        
        self.assertEqual(self.calc.add(-1, 1), 0)

if __name__ == '__main__':
    unittest.main(argv=['first-arg-is-ignored'], exit=False)

Output:

.
----------------------------------------------------------------------
Ran 1 test in 0.000s

OK

Method 2: Validating Exceptions (Error Handling)

A robust method should raise specific errors when given invalid input. Validating behavior includes ensuring that the correct exception is raised under the correct circumstances.

Using assertRaises

We verify that our divide method raises a ValueError when dividing by zero.

import unittest

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

    def test_divide_by_zero_raises_error(self):
        with self.assertRaises(ValueError) as context:
            self.calc.divide(10, 0)
        
        self.assertEqual(str(context.exception), "Cannot divide by zero")

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

if __name__ == '__main__':
    unittest.main(argv=['first-arg-is-ignored'], exit=False)

Output:

..
----------------------------------------------------------------------
Ran 2 tests in 0.000s

OK

Method 3: Validating Interactions with Mocking

Sometimes, methods rely on external systems (APIs, databases, file systems). You don't want your unit tests to actually hit these services. Instead, you use mocks to simulate them and validate that your method interacted with them correctly.

We use unittest.mock to verify behavior.

Validating Function Calls

Imagine a UserManager that sends an email when a user is created.

class UserManager:
    def __init__(self, email_service):
        self.email_service = email_service

    def create_user(self, username, email):
        print(f"User {username} created.")
        self.email_service.send_email(email, "Welcome!")

Then we use unittest.mock:

import unittest
from unittest.mock import Mock

class TestUserManager(unittest.TestCase):
    def test_create_user_sends_email(self):
        mock_email_service = Mock()
        
        manager = UserManager(mock_email_service)
        
        manager.create_user("Alice", "alice@example.com")
        
        mock_email_service.send_email.assert_called_once()
        
        mock_email_service.send_email.assert_called_with("alice@example.com", "Welcome!")

if __name__ == '__main__':
    unittest.main(argv=['first-arg-is-ignored'], exit=False)

Output:

User Alice created.
.
----------------------------------------------------------------------
Ran 1 test in 0.001s

OK

Conclusion

Validating method behaviors ensures code reliability through structured testing.

  1. Unit Tests: Use unittest.TestCase and assertions like assertEqual to verify return values.
  2. Exception Testing: Use assertRaises to ensure your methods handle bad inputs gracefully by raising the expected errors.
  3. Mocking: Use unittest.mock to verify interactions with dependencies without running external code.
Navigation