Advertisement
βœ… Real-World

Python Testing – unittest & pytest

Tests catch bugs before users do. Python offers unittest (standard library) and pytest (industry standard). Learn fixtures, mocking, and coverage.

⏱️ 20 min read🎯 Real-WorldπŸ“… Updated 2026

unittest – Standard Library

Python
import unittest

def divide(a, b):
    if b == 0:
        raise ValueError("Cannot divide by zero")
    return a / b

class TestMath(unittest.TestCase):
    def test_divide_normal(self):
        self.assertAlmostEqual(divide(10, 3), 3.333, places=3)

    def test_divide_zero(self):
        with self.assertRaises(ValueError):
            divide(10, 0)

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

pytest – Industry Standard

Python
import pytest

def add(a, b):
    return a + b

def test_add():
    assert add(2, 3) == 5

@pytest.mark.parametrize("a,b,expected", [
    (2, 3, 5),
    (-1, 1, 0),
    (0, 0, 0),
])
def test_add_parametrize(a, b, expected):
    assert add(a, b) == expected

Fixtures

Python
import pytest
import sqlite3

@pytest.fixture
def db():
    conn = sqlite3.connect(":memory:")
    conn.execute("CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT)")
    yield conn
    conn.close()

def test_insert(db):
    db.execute("INSERT INTO users VALUES (1, 'Alice')")
    result = db.execute("SELECT name FROM users").fetchone()
    assert result[0] == "Alice" 

Mocking

Python
from unittest.mock import patch, MagicMock
import requests

def fetch_user(user_id):
    resp = requests.get(f"https://api.example.com/users/{user_id}")
    return resp.json()

def test_fetch_user():
    with patch("requests.get") as mock_get:
        mock_get.return_value.json.return_value = {"id": 1, "name": "Bob"}
        user = fetch_user(1)
        assert user["name"] == "Bob" 
Bash
pytest -v --cov=mymodule  # pip install pytest-cov
Tip: Name tests test_what_when_expected() β€” e.g., test_divide_by_zero_raises_value_error. Failures become self-documenting.

Testing with pytest: Confidence to Change Code

Tests are code that checks your code. Their real payoff isn't catching today's bug β€” it's letting you refactor fearlessly later, because a green test suite proves you didn't break anything. pytest makes them nearly frictionless: plain functions and plain assert.

# test_math.py β€” pytest auto-discovers test_* functions
def add(a, b): return a + b

def test_add():
    # Arrange – Act – Assert
    result = add(2, 3)          # Act
    assert result == 5          # Assert

import pytest
def test_divide_by_zero():
    with pytest.raises(ZeroDivisionError):   # assert it raises
        1 / 0
ConceptWhat it is
AAA patternArrange, Act, Assert β€” structure of a good test
Fixture@pytest.fixture β€” reusable setup (db, sample data)
Mockfake a slow/external dependency (API, clock)
Parametrizerun one test over many inputs

What to test: behavior, not implementation β€” assert on outputs, not internal steps, so tests survive refactors. Cover the happy path, edge cases (empty, zero, negative), and error cases. Mock external services so tests stay fast and deterministic. Run them on every commit (CI) and one broken test blocks the merge.

πŸ‹οΈ Practical Exercise

Write your first tests:

  1. Write a simple function and a unittest.TestCase that asserts its output.
  2. Rewrite the same test as a plain pytest function using assert.
  3. Create a pytest fixture that provides sample data to multiple tests.
  4. Mock an external call (e.g. a network request) so the test runs offline.

πŸ”₯ Challenge Exercise

Add a test suite to a small module (e.g. a calculator or a validation function): cover normal cases, edge cases, and error cases (using pytest.raises). Use a fixture for shared setup and parametrize a test to run across many inputs with @pytest.mark.parametrize. Bonus: mock a function that talks to an external service and verify your code calls it correctly without hitting the network.

πŸ“‹ Summary

  • Automated tests catch regressions and let you refactor with confidence.
  • unittest is the class-based standard library framework; pytest is the popular, terser third-party choice.
  • Unit tests check small pieces in isolation; integration tests check components working together.
  • Fixtures provide reusable setup/teardown and test data.
  • Mocking replaces external dependencies so tests stay fast and deterministic.
  • Parametrization runs the same test across many inputs; coverage measures tested code but is not a goal in itself.

Interview Questions on Testing

  • Why is automated testing important?
  • What is the difference between unittest and pytest?
  • What is a unit test versus an integration test?
  • What is a fixture and why is it useful?
  • What is mocking and when do you use it?
  • What does test parametrization achieve?
  • What is test coverage and is 100% coverage the goal?

FAQ

Should I use unittest or pytest? +

pytest is the most popular choice: it uses plain assert statements, has powerful fixtures and parametrization, and less boilerplate. unittest ships with Python and is fine too, especially in codebases already using it. pytest can even run unittest tests.

What is mocking? +

Mocking replaces a real dependency (a network call, database, or clock) with a controllable stand-in during a test. This keeps tests fast, deterministic, and isolated, and lets you assert how your code interacts with that dependency.

What is a fixture? +

A fixture is reusable setup code that provides data or resources to tests β€” like a sample object, a temporary database, or a configured client. In pytest you declare it with @pytest.fixture and request it by adding its name as a test argument.

Is 100% test coverage the goal? +

No. Coverage shows which lines ran during tests, but high coverage with weak assertions proves little. Aim to test important behavior, edge cases, and failure paths; meaningful tests matter more than a coverage percentage.