Skip to content

Unit Tests — Common Mistakes

Mistake 1: Test Without an Assertion

The test calls the function, nothing crashes, pytest marks it green. But the result was never checked. Wrong output ships to production.

import pytest


def calculate_shipping(weight_kg: float, express: bool) -> float:
    if weight_kg <= 0:
        raise ValueError("Weight must be positive")
    base = 5.0 + weight_kg * 2.0
    if express:
        base *= 1.5
    return round(base, 2)


@pytest.mark.unit
class TestShippingBad:
    """Bad: calls the function but never checks the result."""

    def test_standard_shipping(self) -> None:
        calculate_shipping(3.0, express=False)  # no assert

    def test_express_shipping(self) -> None:
        calculate_shipping(3.0, express=True)   # no assert

Both tests pass even if someone changes the formula and express now costs 10×. The function ran, it didn't crash — "all good." Meanwhile customers see $165 on checkout instead of $16.50.

@pytest.mark.unit
class TestShippingGood:
    """Good: every test checks the actual value."""

    def test_standard_shipping(self) -> None:
        assert calculate_shipping(3.0, express=False) == 11.0

    def test_express_shipping(self) -> None:
        assert calculate_shipping(3.0, express=True) == 16.5

    def test_minimum_weight(self) -> None:
        assert calculate_shipping(0.1, express=False) == 5.2

    def test_zero_weight_raises(self) -> None:
        with pytest.raises(ValueError, match="Weight must be positive"):
            calculate_shipping(0, express=False)

Rule: every test that calls a function returning a value must assert that value.


Mistake 2: Hidden Integration Dependency

A "unit" test that hits a real database or calls a live API is not a unit test. It passes locally because the developer has Postgres running. It fails on CI where the container isn't there.

import pytest
from myapp.db import get_db_connection


@pytest.mark.unit
class TestUserServiceBad:
    """Bad: hits a real database inside a unit test."""

    def test_user_exists(self) -> None:
        conn = get_db_connection()           # real DB call
        result = conn.execute(
            "SELECT 1 FROM users WHERE id = 1"
        ).fetchone()
        assert result is not None            # fails on CI

Fix: mock the dependency at the boundary.

from unittest.mock import MagicMock, patch

import pytest
from myapp.user_service import UserService


@pytest.mark.unit
class TestUserServiceGood:
    """Good: database is mocked, test runs in isolation."""

    def test_user_exists_returns_true(self) -> None:
        mock_repo = MagicMock()
        mock_repo.find_by_id.return_value = {"id": 1, "name": "Alice"}
        service = UserService(repo=mock_repo)
        assert service.user_exists(user_id=1) is True

    def test_user_not_found_returns_false(self) -> None:
        mock_repo = MagicMock()
        mock_repo.find_by_id.return_value = None
        service = UserService(repo=mock_repo)
        assert service.user_exists(user_id=999) is False

Rule: inject dependencies (repository, HTTP client, file system) so tests can replace them with mocks. Real connections belong in integration tests.


Mistake 3: One Test Covers Everything

A single test that checks the happy path gives 0 information when it fails. Was it the empty case? The uppercase rule? The network error? Nobody knows.

@pytest.mark.unit
def test_everything_at_once() -> None:
    # checks valid, invalid, boundary all in one test
    assert validate_password("Str0ng!Pass") == []
    assert validate_password("weak") != []
    assert validate_password("") != []

One failure masks the rest. Use parametrize to separate concerns:

@pytest.mark.unit
@pytest.mark.parametrize("password, rule", [
    ("Ab1!", "at least 8 characters"),
    ("weak1!pass", "one uppercase letter"),
    ("NoDigits!Here", "one digit"),
    ("NoSpecial1", "one special character"),
])
def test_each_rule_independently(password: str, rule: str) -> None:
    assert rule in validate_password(password)

Rule: one test = one scenario. If it fails, the name tells you exactly what broke.


Checklist

Check Bad Pattern Fix
Every return value asserted Call without assert Always assert result == expected
No real I/O in unit scope DB/HTTP calls in unit tests Mock external dependencies
One scenario per test Many assertions in one test Use parametrize
Error paths tested Only happy path Add pytest.raises for expected exceptions
Coverage enforced in CI Tests merged without coverage --cov-fail-under=95 in CI