Learn how to write fast, reliable tests using the built-in unittest and the popular pytest framework—plus fixtures, mocking, parameterization, coverage, and CI.
1) Why Unit Testing?
Unit tests check small, isolated pieces of code (functions/classes) to catch bugs early. They also document behavior and enable safe refactoring. With a fast test suite, you’ll ship changes with confidence.
2) A Clean Project Structure
Recommended layout
myapp/
├─ src/
│  └─ myapp/
│     ├─ __init__.py
│     ├─ mathy.py
│     └─ io_helpers.py
├─ tests/
│  ├─ test_mathy.py
│  └─ test_io_helpers.py
├─ pyproject.toml
└─ README.md
          Tip: Put application code in src/myapp and tests in tests/. Tools like pytest discover tests named test_*.py.
Example code (to be tested)
# src/myapp/mathy.py
def add(a: float, b: float) -> float:
    return a + b
def divide(a: float, b: float) -> float:
    if b == 0:
        raise ZeroDivisionError("b must not be zero")
    return a / b
        3) The Built-in unittest (Batteries Included)
      unittest ships with Python 3—no install needed.
# tests/test_mathy_unittest.py
import unittest
from myapp.mathy import add, divide
class TestMathy(unittest.TestCase):
    def test_add(self):
        self.assertEqual(add(2, 3), 5)
        self.assertAlmostEqual(add(0.1, 0.2), 0.3, places=7)
    def test_divide_ok(self):
        self.assertEqual(divide(10, 2), 5.0)
    def test_divide_by_zero(self):
        with self.assertRaises(ZeroDivisionError) as cm:
            divide(1, 0)
        self.assertIn("must not be zero", str(cm.exception))
if __name__ == "__main__":
    unittest.main()
      Run: python -m unittest or python -m unittest tests/test_mathy_unittest.py
4) pytest (Fast, Friendly, Powerful)
      Install with pip install pytest then run pytest. It offers concise tests, rich output, and powerful plugins.
# tests/test_mathy_pytest.py
from myapp.mathy import add, divide
import pytest
def test_add():
    assert add(2, 3) == 5
def test_divide_ok():
    assert divide(9, 3) == 3.0
def test_divide_by_zero():
    with pytest.raises(ZeroDivisionError):
        divide(1, 0)
      Run: pytest -q
5) Fixtures: Reusable Setup/Teardown
Fixtures provide test data or resources and clean up automatically.
# tests/conftest.py
import tempfile, shutil, pathlib, pytest
@pytest.fixture
def temp_dir():
    d = tempfile.mkdtemp()
    try:
        yield pathlib.Path(d)
    finally:
        shutil.rmtree(d)
# tests/test_io_helpers.py
def test_write_and_read(temp_dir):
    p = temp_dir / "hello.txt"
    p.write_text("hi")
    assert p.read_text() == "hi"
      Tip: Place shared fixtures in tests/conftest.py for auto-discovery.
6) Mocking with unittest.mock
      Mock external calls (network, file system, time) so tests are fast and deterministic.
# src/myapp/io_helpers.py
import requests
def fetch_title(url: str) -> str:
    r = requests.get(url, timeout=5)
    r.raise_for_status()
    return r.text.split("<title>")[1].split("</title>")[0]
# tests/test_io_helpers_mock.py
from unittest.mock import patch, MagicMock
from myapp.io_helpers import fetch_title
@patch("myapp.io_helpers.requests.get")
def test_fetch_title_mocks_requests(mock_get):
    fake = MagicMock()
    fake.text = "<html><title>Hello</title></html>"
    fake.raise_for_status = lambda: None
    mock_get.return_value = fake
    assert fetch_title("https://example.com") == "Hello"
    mock_get.assert_called_once()
      Rule of thumb: Patch where the object is used, not where it’s defined.
7) Parameterization (More Cases, Less Boilerplate)
# tests/test_mathy_param.py
import pytest
from myapp.mathy import add
@pytest.mark.parametrize(
    "a,b,expected",
    [
        (0, 0, 0),
        (2, 3, 5),
        (-1, 5, 4),
        (0.1, 0.2, 0.3),
    ],
)
def test_add_param(a, b, expected):
    assert add(a, b) == pytest.approx(expected)
      Why it rocks: Easier to read, great failure messages, and one test turns into many.
8) Coverage: Know What You Tested
Install pip install coverage, then:
# with pytest
coverage run -m pytest
coverage report -m
coverage html  # open htmlcov/index.html
      Aim for meaningful coverage (e.g., 80%+), without gaming the metric.
9) A Tiny TDD Loop
- Write a small failing test (red).
 - Implement the simplest code to pass (green).
 - Refactor for clarity/perf while keeping tests green.
 
# 1) failing test first
def test_is_even():  # tests/test_even.py
    from myapp.mathy import is_even
    assert is_even(2) is True
    assert is_even(3) is False
# 2) minimal implementation
# src/myapp/mathy.py
def is_even(n: int) -> bool:
    return (n % 2) == 0
    10) Run Tests on Every Push (CI)
Here’s a minimal GitHub Actions workflow to run tests on Python 3.10–3.12 and generate coverage.
# .github/workflows/tests.yml
name: tests
on: [push, pull_request]
jobs:
  test:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        python-version: ["3.10", "3.11", "3.12"]
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-python@v5
        with: { python-version: ${{ matrix.python-version }} }
      - run: python -m pip install --upgrade pip
      - run: pip install -e . pytest coverage
      - run: coverage run -m pytest -q
      - run: coverage report -m
      Pro tip: Fail the build if coverage drops using coverage report --fail-under=80.
11) Assertions & Cheatsheet
unittest assertions
          | Assertion | Use | 
|---|---|
assertEqual(a, b) | Exact equality | 
assertAlmostEqual(a, b, places=7) | Floats | 
assertTrue(x) / assertFalse(x) | Truthiness | 
assertIn(x, seq) | Membership | 
assertIsNone(x) | None checks | 
assertRaises(E) | Exceptions | 
pytest power-ups
          | Feature | Example | 
|---|---|
| Simple asserts | assert func(x) == y | 
| Parameterize | @pytest.mark.parametrize(...) | 
| Fixtures | @pytest.fixture + autouse | 
| Marks | @pytest.mark.slow, -m "not slow" | 
| Skip/xfail | pytest.skip(), @pytest.mark.xfail | 
12) FAQ
Do I need both unittest and pytest?
      No. Many teams use just pytest for new projects. You can still run legacy unittest tests under pytest.
How many tests should I write?
Focus on critical paths and tricky logic. Aim for high coverage with meaningful scenarios—not just lines executed.
How do I test time, random, or network calls?
Use unittest.mock to patch functions like time.time, random.random, or HTTP clients. Inject dependencies where possible.
Copy-Paste Quickstart
# 1) install
pip install pytest coverage
# 2) create files
mkdir -p src/myapp tests
# add src/myapp/mathy.py and tests/test_mathy_pytest.py (from above)
# 3) run tests + coverage
pytest -q
coverage run -m pytest
coverage report -m
    
No comments:
Post a Comment