Testing
Testing is one of the best habits you can build as a Python developer.
If coding is “building,” then testing is “proof that what you built actually works.”
Good tests help you:
- catch bugs early
- refactor code safely
- document expected behavior
- ship with confidence
In this chapter, you will learn testing from beginner level to practical real-world usage.
What Is Testing?
Section titled “What Is Testing?”Testing means checking whether your code behaves as expected for different inputs and conditions.
Simple example:
def add(a, b): return a + b
assert add(2, 3) == 5If the result is not 5, Python raises an AssertionError.
Why Testing Matters
Section titled “Why Testing Matters”- Prevents regressions (old features breaking after new changes)
- Makes debugging faster
- Helps teams collaborate with fewer surprises
- Acts as executable documentation
When To Use Testing
Section titled “When To Use Testing”Testing is almost always useful, but how much testing you write should match the project stage.
Great times to invest heavily in tests
Section titled “Great times to invest heavily in tests”- Production apps (web apps, APIs, fintech, health, education platforms)
- Shared libraries used by many people
- Code with business rules (pricing, calculations, permissions)
- Long-lived projects with frequent refactors
Early-stage cases where lightweight testing is enough
Section titled “Early-stage cases where lightweight testing is enough”- Tiny throwaway scripts
- One-time automation script for personal use
- Very early prototype where requirements change every hour
Practical rule for beginners
Section titled “Practical rule for beginners”At initial days of a project:
- Write tests for core logic from day 1
- Skip heavy end-to-end tests until flows stabilize
- Add deeper test coverage as the project becomes stable and shared
So testing is still good at the beginning, but start smart, not heavy.
Testing Pyramid
Section titled “Testing Pyramid”- Unit tests: fast, small, most of your tests
- Integration tests: test modules working together
- End-to-end tests: full user flow, slower, fewer
Assertion Testing with assert
Section titled “Assertion Testing with assert”assert is the simplest way to test in Python.
def is_even(n): return n % 2 == 0
assert is_even(4)assert is_even(7) is False- very easy to start
- no setup needed
Limitations
Section titled “Limitations”- poor test organization for large projects
- weak reporting compared to test frameworks
- can be disabled with optimized mode (
python -O)
Use assert for quick checks and learning. For real projects, prefer unittest or pytest.
unittest
Section titled “unittest”unittest comes with Python standard library, so no installation needed.
Basic test case
Section titled “Basic test case”import unittest
def add(a, b): return a + b
class TestMath(unittest.TestCase): def test_add_positive(self): self.assertEqual(add(2, 3), 5)
def test_add_negative(self): self.assertEqual(add(-1, -1), -2)
if __name__ == "__main__": unittest.main()Common assertions
Section titled “Common assertions”self.assertEqual(a, b)self.assertTrue(x)self.assertFalse(x)self.assertIn(item, collection)self.assertRaises(ErrorType)
Setup and Teardown
Section titled “Setup and Teardown”Use setup/teardown when tests need repeated preparation and cleanup.
import unittest
class TestUserDB(unittest.TestCase): def setUp(self): # runs before each test method self.users = []
def tearDown(self): # runs after each test method self.users.clear()
def test_add_user(self): self.users.append("Aman") self.assertEqual(len(self.users), 1)
def test_users_starts_empty(self): self.assertEqual(self.users, [])Class-level setup/teardown
Section titled “Class-level setup/teardown”@classmethod setUpClass(cls)runs once before all tests@classmethod tearDownClass(cls)runs once after all tests
Use these for expensive resources like database connection setup.
pytest
Section titled “pytest”pytest is widely used because it is simple, powerful, and has great plugins.
Install
Section titled “Install”uv add pytestWriting tests in pytest
Section titled “Writing tests in pytest”You can write simple test functions (no class required).
def multiply(a, b): return a * b
def test_multiply_positive_numbers(): assert multiply(3, 4) == 12
def test_multiply_by_zero(): assert multiply(9, 0) == 0Run tests
Section titled “Run tests”uv run pytestUseful commands:
pytest -qfor concise outputpytest -k "name_filter"run matching testspytest -xstop after first failurepytest -vverbose names
Pytest Fixtures
Section titled “Pytest Fixtures”Fixtures help you avoid duplicate setup code.
import pytest
@pytest.fixturedef sample_user(): return {"name": "Sahil", "role": "student"}
def test_user_name(sample_user): assert sample_user["name"] == "Sahil"
def test_user_role(sample_user): assert sample_user["role"] == "student"Fixture scopes
Section titled “Fixture scopes”Test Discovery
Section titled “Test Discovery”Both unittest and pytest can discover tests automatically.
Common naming convention
Section titled “Common naming convention”- file names:
test_*.pyor*_test.py - test functions: start with
test_ - test classes: often
Test...
Run discovery
Section titled “Run discovery”# pytest discoverypytest
# unittest discoverypython -m unittest discoverTesting Exceptions
Section titled “Testing Exceptions”With unittest
Section titled “With unittest”import unittest
def divide(a, b): return a / b
class TestDivide(unittest.TestCase): def test_divide_by_zero(self): with self.assertRaises(ZeroDivisionError): divide(10, 0)With pytest
Section titled “With pytest”import pytest
def divide(a, b): return a / b
def test_divide_by_zero(): with pytest.raises(ZeroDivisionError): divide(10, 0)Monkeypatch and Mocking Input/External Calls
Section titled “Monkeypatch and Mocking Input/External Calls”Real code often depends on things that are hard to test directly, like:
- user input
- current time
- network calls
- environment variables
To test properly, replace those dependencies in tests.
A. pytest monkeypatch for auto input method
Section titled “A. pytest monkeypatch for auto input method”This is useful when your function uses input().
def ask_name(): name = input("Enter name: ") return f"Hello {name}"
def test_ask_name(monkeypatch): monkeypatch.setattr("builtins.input", lambda _: "Aman") assert ask_name() == "Hello Aman"The test now runs automatically without manual typing.
B. unittest.mock.patch for input
Section titled “B. unittest.mock.patch for input”import unittestfrom unittest.mock import patch
def ask_age(): age = input("Enter age: ") return int(age)
class TestInput(unittest.TestCase): @patch("builtins.input", return_value="21") def test_ask_age(self, _mock_input): self.assertEqual(ask_age(), 21)C. Monkeypatch environment variable
Section titled “C. Monkeypatch environment variable”def get_mode(): import os return os.getenv("APP_MODE", "dev")
def test_get_mode(monkeypatch): monkeypatch.setenv("APP_MODE", "test") assert get_mode() == "test"D. Mock network request
Section titled “D. Mock network request”import requests
def fetch_data(url): response = requests.get(url) return response.json()
def test_fetch_data(monkeypatch): class MockResponse: def __init__(self): self.status_code = 200
def json(self): return {"message": "Hello"}
def mock_get(url): # Optional: validate input assert url == "http://fakeurl" return MockResponse()
# Patch requests.get monkeypatch.setattr(requests, "get", mock_get)
result = fetch_data("http://fakeurl")
assert result == {"message": "Hello"}Instead of calling real API in unit tests, mock the request function and return fake data.
This makes tests:
- faster
- reliable
- independent of internet/server uptime
Project Structure Example
Section titled “Project Structure Example”my_project/ src/ calculator.py tests/ test_calculator.py pyproject.tomlKeep tests in a separate tests/ directory for clarity.
Good Testing Habits
Section titled “Good Testing Habits”Arrange-Act-Assert pattern
Section titled “Arrange-Act-Assert pattern”def test_discount_applied_for_premium_user(): # Arrange price = 100 is_premium = True
# Act result = apply_discount(price, is_premium)
# Assert assert result == 90Common Mistakes and How To Avoid Them
Section titled “Common Mistakes and How To Avoid Them”Choosing Between assert, unittest, and pytest
Section titled “Choosing Between assert, unittest, and pytest”| Option | Best For | Pros | Cons |
|---|---|---|---|
assert | Quick checks, learning | simplest | not scalable for big projects |
unittest | Standard library-only projects, enterprise legacy code | built-in, structured | more boilerplate |
pytest | Modern projects of all sizes | concise, powerful fixtures, plugins | extra dependency |
Recommended for most beginners building real projects: start with pytest.
Minimal Testing Plan for New Projects
Section titled “Minimal Testing Plan for New Projects”Week 1:
- test pure utility functions
- test input validation logic
Week 2-3:
- add tests for important workflows (signup, payment calc, API parsing)
Later:
- add integration tests for DB/API boundaries
- add CI (GitHub Actions) to run tests on every push