Integration testing verifies that different parts of your application work together correctly. While unit tests check individual functions in isolation, integration tests check how those functions interact with databases, APIs, file systems, and other services.
Think of it this way: unit tests check that each brick is solid. Integration tests check that the wall holds together.
| Aspect | Unit Tests | Integration Tests |
|---|---|---|
| Scope | Single function/class | Multiple components together |
| Dependencies | Mocked/stubbed | Real or test databases |
| Speed | Fast | Slower (more setup) |
| Purpose | Find logic bugs | Find interaction bugs |
| Example | calculate_gpa() returns 4.0 |
Saving student to DB and reading it back |
Write integration tests when:
# Unit test - no database
def test_calculate_gpa():
assert calculate_gpa([90, 85, 95]) == 4.0
# Integration test - uses real database
def test_create_and_retrieve_student():
create_student("John", "AB123456")
student = get_student("AB123456")
assert student.name == "John"
A fixture is a fixed state of the system under test. Fixtures provide:
Simple setup directly in the test:
import unittest
from datetime import date
class TestStudentRegistration(unittest.TestCase):
"""Test student registration with database."""
def test_register_new_student(self):
"""Test registering a new student."""
# Inline fixture - create test data
student_data = {
"name": "John Doe",
"student_id": "AB123456",
"class": "1A",
"enrollment_date": date(2024, 1, 15)
}
# Use the fixture
student = Student.register(student_data)
# Verify
assert student.name == "John Doe"
assert student.student_id == "AB123456"
Using setUp and tearDown for shared setup:
class TestWithSetup(unittest.TestCase):
"""Test class using setUp fixtures."""
def setUp(self):
"""
Set up test fixtures before each test.
This runs before EVERY test method.
"""
# Create a fresh database connection
self.db = TestDatabase()
self.db.connect()
# Create test data
self.test_student = Student(
name="Test Student",
student_id="TEST001"
)
self.db.add_student(self.test_student)
def tearDown(self):
"""
Clean up after each test.
This runs after EVERY test method.
"""
# Remove test data
self.db.remove_student("TEST001")
# Close connection
self.db.close()
def test_retrieve_student(self):
"""Test retrieving a student from database."""
# setUp has already created the test student
student = self.db.get_student("TEST001")
self.assertEqual(student.name, "Test Student")
def test_update_student(self):
"""Test updating a student in database."""
# Use the test student created in setUp
self.db.update_student("TEST001", name="Updated Name")
student = self.db.get_student("TEST001")
self.assertEqual(student.name, "Updated Name")
Using setUpModule and tearDownModule for one-time setup:
# Runs once before any tests in this module
def setUpModule():
"""Set up test database for all tests."""
global test_db
test_db = TestDatabase()
test_db.initialize()
test_db.connect()
print("Test database initialized")
# Runs once after all tests in this module
def tearDownModule():
"""Clean up test database."""
test_db.disconnect()
test_db.cleanup()
print("Test database cleaned up")
class TestFeature1(unittest.TestCase):
"""Tests that share the module-level database."""
def test_something(self):
# test_db is available from setUpModule
result = test_db.query("SELECT * FROM students")
self.assertIsNotNone(result)
Using setUpClass and tearDownClass for per-class setup:
class TestStudentAPI(unittest.TestCase):
"""Test API endpoints with shared client."""
@classmethod
def setUpClass(cls):
"""
Set up once for all tests in this class.
Good for expensive operations like starting a test server.
"""
cls.test_server = TestServer()
cls.test_server.start()
cls.client = cls.test_server.get_client()
@classmethod
def tearDownClass(cls):
"""Clean up once after all tests in this class."""
cls.test_server.stop()
def test_get_student(self):
"""Test GET /students/{id} endpoint."""
response = self.client.get("/students/AB123456")
self.assertEqual(response.status_code, 200)
def test_create_student(self):
"""Test POST /students endpoint."""
response = self.client.post("/students", json={
"name": "John Doe",
"student_id": "AB123456"
})
self.assertEqual(response.status_code, 201)
Fixtures should be straightforward and easy to understand:
# GOOD: Simple, clear fixture
def setUp(self):
self.student = Student(name="Test", id="TEST001")
self.db.add(self.student)
# BAD: Complex, hard-to-debug fixture
def setUp(self):
self.config = load_config()
self.factory = StudentFactory(self.config)
self.builder = self.factory.get_builder()
self.student = self.builder.with_defaults().build_complex()
self.db.initialize_from_factory(self.factory)
Each test should work regardless of other tests:
# GOOD: Each test creates its own data
def test_update_student(self):
student = self.create_test_student()
student.name = "Updated"
self.db.save(student)
def test_delete_student(self):
student = self.create_test_student() # Fresh data
self.db.delete(student.id)
# BAD: Tests depend on order
def test_setup_data(self):
# This test creates data for other tests
self.shared_student = Student(id="SHARE001")
def test_uses_shared_data(self):
# Breaks if run alone or out of order
result = self.db.get(self.shared_student.id)
# GOOD: Clear what the data represents
def setUp(self):
self.passing_student = Student(
name="Passing Student",
grades=[85, 90, 88],
status="enrolled"
)
self.failing_student = Student(
name="Failing Student",
grades=[45, 50, 48],
status="probation"
)
# BAD: Unclear test data
def setUp(self):
self.s1 = Student("A", [1,2,3], "x")
self.s2 = Student("B", [4,5,6], "y")
import unittest
import sqlite3
import os
class TestDatabaseOperations(unittest.TestCase):
"""Test database operations with fixtures."""
def setUp(self):
"""Set up a fresh in-memory database for each test."""
# Use in-memory database for speed and isolation
self.conn = sqlite3.connect(":memory:")
self.cursor = self.conn.cursor()
# Create the test schema
self.cursor.execute("""
CREATE TABLE students (
student_id TEXT PRIMARY KEY,
name TEXT NOT NULL,
class TEXT NOT NULL,
enrolled_date TEXT
)
""")
self.conn.commit()
# Add some test data
self.test_students = [
("AB123456", "Alice", "1A", "2024-01-15"),
("AB123457", "Bob", "1B", "2024-01-15"),
("AB123458", "Charlie", "1A", "2024-01-16"),
]
self.cursor.executemany(
"INSERT INTO students VALUES (?, ?, ?, ?)",
self.test_students
)
self.conn.commit()
def tearDown(self):
"""Clean up the database connection."""
self.conn.close()
# Tests that use the fixture
def test_get_all_students(self):
"""Test retrieving all students."""
self.cursor.execute("SELECT * FROM students")
result = self.cursor.fetchall()
self.assertEqual(len(result), 3)
self.assertEqual(result[0][1], "Alice")
def test_get_student_by_id(self):
"""Test retrieving a specific student."""
student_id = "AB123456"
self.cursor.execute(
"SELECT * FROM students WHERE student_id = ?",
(student_id,)
)
result = self.cursor.fetchone()
self.assertIsNotNone(result)
self.assertEqual(result[0], "AB123456")
self.assertEqual(result[1], "Alice")
def test_add_new_student(self):
"""Test adding a new student."""
new_student = ("AB123459", "Diana", "1C", "2024-01-17")
self.cursor.execute(
"INSERT INTO students VALUES (?, ?, ?, ?)",
new_student
)
self.conn.commit()
# Verify it was added
self.cursor.execute("SELECT * FROM students")
result = self.cursor.fetchall()
self.assertEqual(len(result), 4)
def test_update_student(self):
"""Test updating student information."""
self.cursor.execute(
"UPDATE students SET class = ? WHERE student_id = ?",
("2A", "AB123456")
)
self.conn.commit()
# Verify the update
self.cursor.execute(
"SELECT class FROM students WHERE student_id = ?",
("AB123456",)
)
result = self.cursor.fetchone()
self.assertEqual(result[0], "2A")
def test_delete_student(self):
"""Test deleting a student."""
self.cursor.execute(
"DELETE FROM students WHERE student_id = ?",
("AB123456",)
)
self.conn.commit()
# Verify deletion
self.cursor.execute(
"SELECT * FROM students WHERE student_id = ?",
("AB123456",)
)
result = self.cursor.fetchone()
self.assertIsNone(result)
import unittest
import tempfile
import shutil
import os
class TestFileOperations(unittest.TestCase):
"""Test file operations with temporary directories."""
def setUp(self):
"""Create a temporary directory for each test."""
# tempfile.mkdtemp creates a unique temporary directory
self.test_dir = tempfile.mkdtemp()
# Create some test files
self.test_file = os.path.join(self.test_dir, "test.txt")
with open(self.test_file, "w") as f:
f.write("Test content")
def tearDown(self):
"""Remove the temporary directory."""
# shutil.rmtree removes the directory and all contents
shutil.rmtree(self.test_dir)
def test_read_file(self):
"""Test reading a file."""
with open(self.test_file, "r") as f:
content = f.read()
self.assertEqual(content, "Test content")
def test_write_file(self):
"""Test writing to a file."""
new_file = os.path.join(self.test_dir, "new.txt")
with open(new_file, "w") as f:
f.write("New content")
# Verify file exists and has content
self.assertTrue(os.path.exists(new_file))
with open(new_file, "r") as f:
content = f.read()
self.assertEqual(content, "New content")
def test_list_directory(self):
"""Test listing directory contents."""
files = os.listdir(self.test_dir)
self.assertEqual(len(files), 1)
self.assertEqual(files[0], "test.txt")
import unittest
from unittest.mock import Mock, patch
class TestStudentService(unittest.TestCase):
"""Test student service with mocked API client."""
def setUp(self):
"""Set up mocked API client."""
# Create a mock client
self.mock_client = Mock()
# Configure default responses
self.mock_client.get_student.return_value = {
"student_id": "AB123456",
"name": "Alice",
"class": "1A"
}
# Create service with mock client
self.service = StudentService(client=self.mock_client)
def test_get_student_data(self):
"""Test retrieving student data."""
student = self.service.get_student("AB123456")
# Verify the service used the client correctly
self.mock_client.get_student.assert_called_once_with("AB123456")
# Verify the data
self.assertEqual(student["name"], "Alice")
def test_get_student_not_found(self):
"""Test handling of student not found."""
# Override the default response
self.mock_client.get_student.return_value = None
student = self.service.get_student("NONEXIST")
self.assertIsNone(student)
def test_create_student(self):
"""Test creating a new student."""
new_student = {
"student_id": "AB123457",
"name": "Bob",
"class": "1B"
}
self.service.create_student(new_student)
# Verify the client was called correctly
self.mock_client.create_student.assert_called_once_with(
student_id="AB123457",
name="Bob",
class="1B"
)
import unittest
from datetime import date
class TestStudentRegistrationFlow(unittest.TestCase):
"""Integration test for complete student registration."""
def setUp(self):
"""Set up database and email service."""
self.db = TestDatabase()
self.db.initialize()
self.db.connect()
self.email_service = MockEmailService()
self.email_service.clear_messages()
self.registration = StudentRegistration(
database=self.db,
email_service=self.email_service
)
def tearDown(self):
"""Clean up database and email service."""
self.db.cleanup()
self.email_service.clear_messages()
def test_complete_registration_flow(self):
"""Test the entire registration process."""
# Arrange
student_data = {
"name": "Alice Johnson",
"student_id": "AB123456",
"email": "alice@nyjc.edu.sg",
"class": "1A",
"enrollment_date": date.today()
}
# Act - register the student
result = self.registration.register(student_data)
# Assert - verify the flow completed
self.assertTrue(result.success)
# Verify student was saved to database
saved_student = self.db.get_student("AB123456")
self.assertEqual(saved_student.name, "Alice Johnson")
# Verify welcome email was sent
emails = self.email_service.get_messages()
self.assertEqual(len(emails), 1)
self.assertEqual(emails[0].to, "alice@nyjc.edu.sg")
self.assertIn("Welcome", emails[0].subject)
def test_registration_duplicate_id(self):
"""Test registration with duplicate student ID."""
# Register first student
first_student = {
"name": "Alice",
"student_id": "AB123456",
"email": "alice@nyjc.edu.sg",
"class": "1A"
}
self.registration.register(first_student)
# Try to register with same ID
second_student = {
"name": "Bob",
"student_id": "AB123456", # Duplicate!
"email": "bob@nyjc.edu.sg",
"class": "1B"
}
result = self.registration.register(second_student)
# Should fail
self.assertFalse(result.success)
self.assertIn("duplicate", result.error.lower())
# Only Alice should be in database
count = self.db.count_students()
self.assertEqual(count, 1)
class TestAttendanceSystem(unittest.TestCase):
"""Integration tests for attendance tracking."""
def setUp(self):
"""Set up database and attendance tracker."""
self.db = TestDatabase()
self.db.initialize()
self.db.connect()
# Create test data
self.db.add_class("1A", "Mathematics")
self.db.add_student("AB123456", "Alice", "1A")
self.db.add_student("AB123457", "Bob", "1A")
self.attendance = AttendanceSystem(database=self.db)
def tearDown(self):
"""Clean up."""
self.db.cleanup()
def test_mark_and_retrieve_attendance(self):
"""Test marking attendance and retrieving records."""
# Mark attendance
self.attendance.mark(
student_id="AB123456",
date="2024-03-15",
status="present"
)
# Retrieve from database
record = self.db.get_attendance("AB123456", "2024-03-15")
self.assertIsNotNone(record)
self.assertEqual(record.status, "present")
def test_class_attendance_summary(self):
"""Test getting attendance summary for a class."""
# Mark attendance for multiple students
self.attendance.mark("AB123456", "2024-03-15", "present")
self.attendance.mark("AB123457", "2024-03-15", "absent")
# Get summary
summary = self.attendance.get_class_summary(
class_id="1A",
date="2024-03-15"
)
self.assertEqual(summary.total, 2)
self.assertEqual(summary.present, 1)
self.assertEqual(summary.absent, 1)
def test_attendance_percentage_calculation(self):
"""Test attendance percentage calculation."""
# Mark attendance for 10 days
for day in range(1, 11):
date_str = f"2024-03-{day:02d}"
status = "present" if day < 8 else "absent"
self.attendance.mark("AB123456", date_str, status)
# Get attendance record
record = self.attendance.get_student_record("AB123456")
# 7 present out of 10 days = 70%
self.assertEqual(record.total_days, 10)
self.assertEqual(record.present_days, 7)
self.assertEqual(record.percentage, 70.0)
# Run integration tests using the project's test runner
poetry run python tests/run_tests.py integration
# Run with verbose output
poetry run python tests/run_tests.py integration -v
# Run specific test class
poetry run python -m unittest test_student_integration.TestStudentRegistrationFlow