AtomLearnAtomLearn
DashboardGoalsPathAchievementsReviewSign In
DashboardTestingTesting Fundamentals
TestingNot Started

Testing Fundamentals

Unit vs integration tests, the Arrange-Act-Assert pattern, and why tests matter

0%
Knowledge0%
Learn & Drill
Fluency0%
Drill & Speed
Retention0%
Mastery & Review
Confidence0%
All modes
Practice
Knowledge
Fluency
Retention

Explanation

A test is code that calls your code and verifies the output is what you expected. Tests catch bugs before users do — and let you refactor with confidence.

Unit vs integration tests:

| Type | What it tests | Speed | Dependencies | |---|---|---|---| | Unit | One function or class in isolation | Fast (ms) | Mocked | | Integration | Multiple components together (e.g. route + DB) | Slower | Real |

Start with unit tests — they run in milliseconds and pinpoint exactly what broke.

The Arrange-Act-Assert (AAA) pattern:

python def test_add_two_numbers(): # Arrange — set up the inputs a, b = 3, 4 # Act — call the unit under test result = add(a, b) # Assert — verify the output assert result == 7 `` Keep each test focused on one behavior. If a test covers three things, it becomes hard to tell what broke.

pytest basics — writing and running tests:

python # test_math.py def add(a, b): return a + b def test_add_positive(): assert add(2, 3) == 5 def test_add_negative(): assert add(-1, 1) == 0 def test_add_zero(): assert add(0, 0) == 0 Run with: pytest test_math.py`

Rules pytest uses to discover tests: - Files named test_*.py or *_test.py - Functions starting with test_ - Classes starting with Test (no __init__)

Testing for exceptions:

python import pytest def divide(a, b): if b == 0: raise ValueError("Cannot divide by zero") return a / b def test_divide_by_zero(): with pytest.raises(ValueError, match="Cannot divide by zero"): divide(10, 0) pytest.raises` asserts that the block raises that specific exception — the test fails if it *doesn't* raise.

Assertion messages — helpful failures:

python def test_user_is_active(): user = get_user(1) assert user.active, f"Expected user {user.id} to be active, got {user.active}" `` The string after the comma is printed when the assertion fails — saves time debugging.

Examples

AAA pattern on a real function

Each test is named for the behavior it verifies; the "happy path" and error path are separate tests

# src/cart.py
def apply_discount(price: float, pct: float) -> float:
    """Apply a percentage discount. pct is 0–100."""
    if pct < 0 or pct > 100:
        raise ValueError(f"Invalid discount: {pct}")
    return price * (1 - pct / 100)

# tests/test_cart.py
import pytest
from src.cart import apply_discount

def test_ten_percent_off():
    # Arrange
    price, pct = 100.0, 10.0
    # Act
    result = apply_discount(price, pct)
    # Assert
    assert result == 90.0

def test_zero_discount():
    assert apply_discount(50.0, 0) == 50.0

def test_invalid_discount_raises():
    with pytest.raises(ValueError):
        apply_discount(100.0, 110)

Unit test vs integration test boundary

Unit tests are fast and run constantly; integration tests are slower and typically run in CI

# Unit test — tests only the logic, no DB, no network
def test_format_username():
    assert format_username("Alice Smith") == "alice_smith"
    assert format_username("  bob  ") == "bob"

# Integration test — tests the route + real DB together
# (usually kept in a separate test file/folder)
def test_create_user_endpoint(client, db_session):
    response = client.post("/users", json={"name": "Alice"})
    assert response.status_code == 201
    assert db_session.query(User).count() == 1

How well did you understand this?

Next in Testing

pytest: Fixtures & Parametrize

Continue