Skip to main content

Python Testing Frameworks

This guide covers Python testing frameworks, best practices, and strategies for writing effective tests.

🎯 Testing Fundamentals

Testing Pyramid

  • Unit Tests: Fast, isolated tests for individual components
  • Integration Tests: Test component interactions
  • End-to-End Tests: Test complete user workflows
  • Performance Tests: Load and stress testing

Testing Principles

  • AAA Pattern: Arrange, Act, Assert
  • First Principles: Fast, Independent, Repeatable, Self-validating, Timely
  • Test Coverage: Aim for high coverage of critical paths
  • Test Naming: Descriptive test names that explain behavior

🧪 pytest Framework

pytest Basics

# test_calculator.py
import pytest
from calculator import Calculator

class TestCalculator:

def setup_method(self):
"""Setup before each test"""
self.calculator = Calculator()

def teardown_method(self):
"""Cleanup after each test"""
pass

def test_add_positive_numbers(self):
"""Test adding two positive numbers"""
# Arrange
a = 5
b = 3

# Act
result = self.calculator.add(a, b)

# Assert
assert result == 8, "5 + 3 should equal 8"

def test_add_negative_numbers(self):
"""Test adding two negative numbers"""
# Arrange
a = -5
b = -3

# Act
result = self.calculator.add(a, b)

# Assert
assert result == -8, "-5 + -3 should equal -8"

def test_divide_by_zero_raises_exception(self):
"""Test that division by zero raises exception"""
# Arrange
a = 10
b = 0

# Act & Assert
with pytest.raises(ZeroDivisionError):
self.calculator.divide(a, b)

@pytest.mark.parametrize("a,b,expected", [
(1, 2, 3),
(0, 5, 5),
(-1, 1, 0),
(100, 200, 300)
])
def test_add_multiple_cases(self, a, b, expected):
"""Test addition with multiple parameter sets"""
result = self.calculator.add(a, b)
assert result == expected

pytest Fixtures

# conftest.py
import pytest
import tempfile
import os

@pytest.fixture
def sample_data():
"""Fixture providing sample data"""
return [1, 2, 3, 4, 5]

@pytest.fixture
def temp_file():
"""Fixture providing a temporary file"""
temp_fd, temp_path = tempfile.mkstemp()
try:
with os.fdopen(temp_fd, 'w') as f:
f.write("test data")
yield temp_path
finally:
os.unlink(temp_path)

@pytest.fixture(scope="session")
def database_connection():
"""Session-scoped database connection"""
connection = create_database_connection()
yield connection
connection.close()

# test_data_processing.py
def test_process_sample_data(sample_data):
"""Test processing sample data"""
result = process_data(sample_data)
assert len(result) == 5

def test_file_operations(temp_file):
"""Test file operations"""
with open(temp_file, 'r') as f:
content = f.read()
assert content == "test data"

pytest Markers and Skipping

import pytest
import sys

@pytest.mark.slow
def test_slow_operation():
"""Test that takes a long time"""
import time
time.sleep(5)
assert True

@pytest.mark.skipif(sys.version_info < (3, 8), reason="Requires Python 3.8+")
def test_python_38_feature():
"""Test feature only available in Python 3.8+"""
import math
result = math.prod([1, 2, 3, 4])
assert result == 24

@pytest.mark.xfail(reason="Known issue, will be fixed in v2.0")
def test_known_failing_feature():
"""Test that currently fails but is expected"""
assert False # This test is expected to fail

@pytest.mark.parametrize("input_data,expected", [
("[email protected]", True),
("invalid-email", False),
("", False),
("@domain.com", False)
])
def test_email_validation(input_data, expected):
"""Test email validation with multiple inputs"""
result = validate_email(input_data)
assert result == expected

pytest Plugins

# pytest.ini
[tool:pytest]
addopts = --verbose --tb=short
testpaths = tests
python_files = test_*.py
python_classes = Test*
python_functions = test_*
markers =
slow: marks tests as slow (deselect with '-m "not slow"')
integration: marks tests as integration tests
unit: marks tests as unit tests

# Using pytest-cov for coverage
# pip install pytest-cov
# Run with: pytest --cov=myapp tests/

# Using pytest-mock for mocking
# pip install pytest-mock
def test_with_mock(mocker):
mock_function = mocker.patch('module.function')
mock_function.return_value = 42

result = module.function()
assert result == 42
mock_function.assert_called_once()

🎭 unittest Framework

unittest Basics

# test_calculator_unittest.py
import unittest
from calculator import Calculator

class TestCalculator(unittest.TestCase):

def setUp(self):
"""Setup before each test"""
self.calculator = Calculator()

def tearDown(self):
"""Cleanup after each test"""
pass

def test_add_positive_numbers(self):
"""Test adding two positive numbers"""
result = self.calculator.add(5, 3)
self.assertEqual(result, 8)

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

def test_divide_by_zero(self):
"""Test that division by zero raises exception"""
with self.assertRaises(ZeroDivisionError):
self.calculator.divide(10, 0)

def test_float_comparison(self):
"""Test floating point comparison"""
result = self.calculator.divide(1, 3)
self.assertAlmostEqual(result, 0.333, places=2)

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

unittest Test Suite

# test_suite.py
import unittest
from test_calculator import TestCalculator
from test_string_utils import TestStringUtils

def create_test_suite():
"""Create a test suite with multiple test cases"""
suite = unittest.TestSuite()

# Add test cases
suite.addTest(unittest.makeSuite(TestCalculator))
suite.addTest(unittest.makeSuite(TestStringUtils))

return suite

if __name__ == '__main__':
runner = unittest.TextTestRunner(verbosity=2)
suite = create_test_suite()
result = runner.run(suite)

🌐 Django Testing

Django Test Framework

# tests/test_models.py
from django.test import TestCase
from django.contrib.auth.models import User
from blog.models import Post, Comment

class PostModelTest(TestCase):

def setUp(self):
"""Setup test data"""
self.user = User.objects.create_user(
username='testuser',
password='testpass123'
)
self.post = Post.objects.create(
title='Test Post',
content='Test content',
author=self.user
)

def test_post_creation(self):
"""Test post creation"""
self.assertEqual(self.post.title, 'Test Post')
self.assertEqual(self.post.author, self.user)
self.assertEqual(str(self.post), 'Test Post')

def test_post_slug_creation(self):
"""Test automatic slug creation"""
self.assertIsNotNone(self.post.slug)
self.assertTrue(len(self.post.slug) > 0)

# tests/test_views.py
from django.test import Client
from django.urls import reverse
from django.contrib.auth.models import User

class PostViewTest(TestCase):

def setUp(self):
"""Setup test data"""
self.client = Client()
self.user = User.objects.create_user(
username='testuser',
password='testpass123'
)
self.post = Post.objects.create(
title='Test Post',
content='Test content',
author=self.user
)

def test_post_list_view(self):
"""Test post list view"""
response = self.client.get(reverse('post-list'))
self.assertEqual(response.status_code, 200)
self.assertContains(response, 'Test Post')

def test_post_detail_view(self):
"""Test post detail view"""
response = self.client.get(
reverse('post-detail', kwargs={'pk': self.post.pk})
)
self.assertEqual(response.status_code, 200)
self.assertContains(response, 'Test Post')

def test_create_post_view(self):
"""Test post creation view"""
self.client.login(username='testuser', password='testpass123')
response = self.client.post(reverse('post-create'), {
'title': 'New Post',
'content': 'New content'
})
self.assertEqual(response.status_code, 302)
self.assertTrue(Post.objects.filter(title='New Post').exists())

Django API Testing

# tests/test_api.py
from django.test import TestCase
from django.contrib.auth.models import User
from rest_framework.test import APIClient
from rest_framework import status
from blog.models import Post
from blog.serializers import PostSerializer

class PostAPITest(TestCase):

def setUp(self):
"""Setup test data"""
self.client = APIClient()
self.user = User.objects.create_user(
username='testuser',
password='testpass123'
)
self.post = Post.objects.create(
title='Test Post',
content='Test content',
author=self.user
)

def test_get_post_list(self):
"""Test getting post list"""
response = self.client.get('/api/posts/')
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(len(response.data), 1)

def test_create_post(self):
"""Test creating a post"""
self.client.force_authenticate(user=self.user)
data = {
'title': 'New Post',
'content': 'New content'
}
response = self.client.post('/api/posts/', data)
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
self.assertEqual(Post.objects.count(), 2)

def test_update_post(self):
"""Test updating a post"""
self.client.force_authenticate(user=self.user)
data = {
'title': 'Updated Post',
'content': 'Updated content'
}
response = self.client.put(
f'/api/posts/{self.post.pk}/',
data
)
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.post.refresh_from_db()
self.assertEqual(self.post.title, 'Updated Post')

📊 Performance Testing

Load Testing with Locust

# locustfile.py
from locust import HttpUser, task, between
import json

class WebsiteUser(HttpUser):
wait_time = between(1, 3)

def on_start(self):
"""Called when a user starts"""
response = self.client.get("/api/auth/login/")
self.token = response.json()['token']

@task(3)
def view_posts(self):
"""View posts (3x more likely)"""
self.client.get("/api/posts/")

@task(1)
def create_post(self):
"""Create a post"""
headers = {'Authorization': f'Bearer {self.token}'}
data = {
'title': 'Load Test Post',
'content': 'This is a load test post'
}
self.client.post("/api/posts/", json=data, headers=headers)

@task(2)
def view_post_detail(self):
"""View post details"""
post_id = 1 # In real scenario, randomly select
self.client.get(f"/api/posts/{post_id}/")

# Run with: locust -f locustfile.py

Benchmark Testing with pytest-benchmark

# test_performance.py
import pytest
from calculator import Calculator

@pytest.mark.benchmark
def test_add_performance(benchmark):
"""Benchmark addition operation"""
calculator = Calculator()
result = benchmark(calculator.add, 100, 200)
assert result == 300

@pytest.mark.benchmark
def test_string_concatenation(benchmark):
"""Benchmark string concatenation methods"""
strings = ['hello', 'world', 'test', 'benchmark']

def concat_with_plus():
result = ''
for s in strings:
result += s
return result

def concat_with_join():
return ''.join(strings)

# Benchmark both methods
plus_result = benchmark(concat_with_plus)
join_result = benchmark(concat_with_join)

assert plus_result == join_result

🛠️ Mocking and Patching

unittest.mock

# test_with_mocks.py
from unittest.mock import Mock, patch, MagicMock
import requests
from api_client import APIClient

class TestAPIClient:

def test_get_data_with_mock(self):
"""Test API client with mocked requests"""
# Create mock response
mock_response = Mock()
mock_response.json.return_value = {'data': 'test'}
mock_response.status_code = 200

# Patch requests.get
with patch('requests.get', return_value=mock_response) as mock_get:
client = APIClient()
result = client.get_data('https://api.example.com/data')

# Assertions
assert result == {'data': 'test'}
mock_get.assert_called_once_with('https://api.example.com/data')

def test_database_operations_with_mock(self):
"""Test database operations with mocked database"""
mock_db = MagicMock()
mock_db.get_user.return_value = {'id': 1, 'name': 'Test User'}

user_service = UserService(mock_db)
user = user_service.get_user(1)

assert user['name'] == 'Test User'
mock_db.get_user.assert_called_once_with(1)

@patch('time.time')
def test_timestamp_generation(self, mock_time):
"""Test timestamp generation with patched time"""
mock_time.return_value = 1234567890

timestamp = generate_timestamp()
assert timestamp == 1234567890
mock_time.assert_called_once()

pytest-mock

# test_with_pytest_mock.py
import pytest
from unittest.mock import Mock

def test_with_pytest_mock(mocker):
"""Test using pytest-mock fixture"""
# Mock external API
mock_api = mocker.patch('api_client.requests.get')
mock_api.return_value.json.return_value = {'status': 'success'}

client = APIClient()
result = client.check_status()

assert result['status'] == 'success'
mock_api.assert_called_once()

def test_spy_on_method(mocker):
"""Test spying on method calls"""
calculator = Calculator()

# Spy on the add method
spy = mocker.spy(calculator, 'add')

result = calculator.add(2, 3)

assert result == 5
spy.assert_called_once_with(2, 3)

📈 Test Coverage

Coverage Analysis

# Run coverage with pytest-cov
# pip install pytest-cov

# Commands:
# pytest --cov=myapp tests/
# pytest --cov=myapp --cov-report=html tests/
# pytest --cov=myapp --cov-report=term-missing tests/

# pytest.ini configuration
[tool:pytest]
addopts = --cov=myapp --cov-report=term-missing --cov-report=html

Coverage Configuration

# .coveragerc
[run]
source = myapp
omit =
*/tests/*
*/venv/*
*/migrations/*
*/__pycache__/*

[report]
exclude_lines =
pragma: no cover
def __repr__
raise AssertionError
raise NotImplementedError
if __name__ == .__main__.:
if TYPE_CHECKING:

🎯 Testing Best Practices

Test Structure

class TestBestPractices:
"""Example of well-structured tests"""

def test_descriptive_name_explains_behavior(self):
"""
Test name should describe what is being tested and expected behavior.
Follow AAA pattern: Arrange, Act, Assert.
"""
# Arrange - set up test data and mocks
calculator = Calculator()
input_a = 5
input_b = 3

# Act - call the method under test
result = calculator.add(input_a, input_b)

# Assert - verify the result
assert result == 8, f"Expected 8 but got {result}"

def test_multiple_assertions_with_descriptive_messages(self):
"""Multiple assertions with clear messages"""
result = some_complex_operation()

assert result is not None, "Result should not be None"
assert result.status == "success", f"Expected success but got {result.status}"
assert len(result.data) > 0, "Result data should not be empty"

@pytest.mark.parametrize("input_data,expected", [
(1, 2),
(2, 4),
(3, 6),
])
def test_parameterized_test_with_clear_cases(self, input_data, expected):
"""Parameterized tests with clear test cases"""
result = double_value(input_data)
assert result == expected

Test Data Management

# factories.py - Using factory pattern for test data
import factory
from django.contrib.auth.models import User
from blog.models import Post

class UserFactory(factory.django.DjangoModelFactory):
class Meta:
model = User

username = factory.Sequence(lambda n: f"user{n}")
email = factory.LazyAttribute(lambda obj: f"{obj.username}@example.com")
password = factory.PostGenerationMethodCall('set_password')

class PostFactory(factory.django.DjangoModelFactory):
class Meta:
model = Post

title = factory.Faker('sentence')
content = factory.Faker('paragraph')
author = factory.SubFactory(UserFactory)

# Usage in tests
def test_with_factory_data():
user = UserFactory()
post = PostFactory(author=user)

assert post.author == user
assert len(post.title) > 0

🔗 Language-Specific Testing

📍 Visit Us

🏫 VD Computer Tuition Surat

VD Computer Tuition
📍 Address
2/66 Faram Street, Rustompura
Surat395002, Gujarat, India
📞 Phone / WhatsApp
+91 84604 41384
🌐 Website

Computer Classes & Tuition — Areas We Serve in Surat

AdajanAlthanAmroliAthwaAthwalinesBhagalBhatarBhestanCanal RoadChowkCitylightDumasGaurav PathGhod Dod RoadHaziraJahangirpuraKamrejKapodraKatargamLimbayatMagdallaMajura GateMota VarachhaNanpuraNew CitylightOlpadPalPandesaraParle PointPiplodPunaRanderRing RoadRustampuraSachinSalabatpuraSarthanaSosyo CircleUdhnaVarachhaVed RoadVesuVIP Road
📞 Call Sir💬 WhatsApp Sir