AlgoDaily

Testing and Automation

Read at your own pace. Code execution, quizzes, progress tracking, and the AI tutor are available in the interactive view with JavaScript enabled.

Final Backend Safety Net

Why this lesson matters

You have already moved through the senior backend arc:

  • Introduction to Databases gave you durable state and transactions.
  • System Design Principles gave you tradeoff-driven thinking.
  • Backend Architecture Patterns gave you boundaries and flow.
  • Advanced Languages and Frameworks showed how those choices become production code.

Now we close the loop with Testing and Automation: the discipline that lets a backend team change systems confidently without relying on heroics.

A senior backend engineer does not just write tests. They design feedback systems: fast enough for daily development, deep enough to catch real failures, and automated enough to protect production.

Final Backend Safety Net

Test For Signal

Tests are an information system

A test suite should answer one question quickly:

Did this change break an important promise?

Senior engineers optimize tests for signal, not just coverage.

The practical testing layers
          Few, expensive, realistic
        ┌───────────────────────────┐
        │ End-to-end / workflow     │
        ├───────────────────────────┤
        │ Integration / adapter     │
        ├───────────────────────────┤
        │ Unit / domain logic       │
        └───────────────────────────┘
          Many, cheap, focused

This connects directly to Backend Architecture Patterns: good boundaries make useful tests easier. If your core logic is portable, you can test business behavior without booting the whole framework.

What senior-level tests prioritize
  • Behavior over implementation details
  • Fast feedback for common changes
  • Realistic checks at risky boundaries
  • Failures that are easy to diagnose
  • Automation that runs the same way for everyone

Test For Signal

Executable Specifications

Unit tests document behavior

For core backend logic, tests should read like executable specifications.

Notice the style below:

  • The test names describe business expectations.
  • The function has no framework dependency.
  • The edge case is explicit.
  • Running the file gives immediate feedback.

This is the payoff from Advanced Languages and Frameworks: keep the core portable, then test it without waiting for HTTP servers, queues, or databases.

```python
from dataclasses import dataclass

@dataclass(frozen=True)
class LineItem:
    sku: str
    quantity: int
    unit_price_cents: int


def calculate_total_cents(items, discount_cents=0):
    subtotal = sum(item.quantity * item.unit_price_cents for item in items)
    total = subtotal - discount_cents
    return max(total, 0)


def test_total_sums_line_items():
    items = [
        LineItem("book", 2, 1500),
        LineItem("pen", 3, 200),
    ]
    assert calculate_total_cents(items) == 3600


def test_total_never_goes_negative():
    items = [LineItem("sticker", 1, 300)]
    assert calculate_total_cents(items, discount_cents=1000) == 0


if __name__ == "__main__":
    tests = [test_total_sums_line_items, test_total_never_goes_negative]
    for test in tests:
        test()
        print(f"PASS: {test.__name__}")
```

Pick The Strategy

A backend team has a modular monolith with clear domain services, database adapters, and HTTP handlers. They want faster, more reliable delivery.

Which testing strategy best matches senior backend practice?

Integration Boundaries

Test the places where reality enters

Unit tests are excellent for pure logic, but backend systems fail at boundaries:

  • SQL constraints
  • transaction behavior
  • serialization formats
  • framework lifecycle hooks
  • message queues
  • third-party APIs

From Introduction to Databases, you already know transactions protect meaning. Integration tests verify that your code and the database actually preserve that meaning together.

The example below uses SQLite from Python's standard library to test a small money transfer boundary. In a real production service, you might run these tests against a disposable Postgres container, but the principle is the same: verify behavior at the persistence boundary.

```python
import sqlite3


def setup_db():
    conn = sqlite3.connect(":memory:")
    conn.execute("CREATE TABLE accounts (id TEXT PRIMARY KEY, balance INTEGER NOT NULL)")
    conn.executemany(
        "INSERT INTO accounts (id, balance) VALUES (?, ?)",
        [("alice", 1000), ("bob", 500)],
    )
    return conn


def transfer(conn, from_id, to_id, cents):
    with conn:
        from_balance = conn.execute(
            "SELECT balance FROM accounts WHERE id = ?", (from_id,)
        ).fetchone()[0]

        if from_balance < cents:
            raise ValueError("insufficient funds")

        conn.execute("UPDATE accounts SET balance = balance - ? WHERE id = ?", (cents, from_id))
        conn.execute("UPDATE accounts SET balance = balance + ? WHERE id = ?", (cents, to_id))


def balance(conn, account_id):
    return conn.execute(
        "SELECT balance FROM accounts WHERE id = ?", (account_id,)
    ).fetchone()[0]


if __name__ == "__main__":
    db = setup_db()
    transfer(db, "alice", "bob", 250)

    assert balance(db, "alice") == 750
    assert balance(db, "bob") == 750

    try:
        transfer(db, "alice", "bob", 10_000)
    except ValueError as error:
        print(f"Expected failure: {error}")

    print("Final balances:", {"alice": balance(db, "alice"), "bob": balance(db, "bob")})
    print("PASS: transfer integration behavior")
```

Coverage Is Enough

True or false?

If a backend service has 100% unit test coverage, integration tests around databases, queues, and external APIs are optional.

Contracts Over Mocks

Mocks should protect contracts, not fantasies

Mocks are useful when they represent a real boundary. They become dangerous when they only mirror what the current implementation happens to call.

A better mental model:

Core service  →  Port / interface  →  Adapter  →  External system

This echoes Hexagonal Architecture from the architecture lesson. Your core depends on a stable port. Tests can use a fake adapter to verify business behavior, while separate integration or contract tests verify the real adapter.

The code below keeps the checkout service focused on its promise: charge the correct amount and return a receipt.

```python
from dataclasses import dataclass
from typing import Protocol


class PaymentGateway(Protocol):
    def charge(self, customer_id: str, amount_cents: int) -> str:
        ...


@dataclass(frozen=True)
class Order:
    customer_id: str
    total_cents: int


class FakePaymentGateway:
    def __init__(self):
        self.charges = []

    def charge(self, customer_id: str, amount_cents: int) -> str:
        self.charges.append((customer_id, amount_cents))
        return "fake_receipt_123"


def checkout(order: Order, gateway: PaymentGateway) -> dict:
    if order.total_cents <= 0:
        raise ValueError("order total must be positive")

    receipt_id = gateway.charge(order.customer_id, order.total_cents)
    return {"status": "paid", "receipt_id": receipt_id}


if __name__ == "__main__":
    gateway = FakePaymentGateway()
    result = checkout(Order("cust_42", 1999), gateway)

    assert result == {"status": "paid", "receipt_id": "fake_receipt_123"}
    assert gateway.charges == [("cust_42", 1999)]

    print("Checkout result:", result)
    print("Gateway calls:", gateway.charges)
    print("PASS: checkout respects payment contract")
```

Name The Gate

In CI/CD automation, a required rule that must pass before code can merge or deploy is commonly called a ____.

Automate The Path

CI/CD turns discipline into default behavior

Automation matters because humans are inconsistent under pressure. A senior backend team builds a path where the safest behavior is also the easiest behavior.

A practical backend pipeline
Commit
  ↓
Format + lint
  ↓
Unit tests
  ↓
Integration tests
  ↓
Build artifact
  ↓
Security + dependency checks
  ↓
Deploy to staging
  ↓
Smoke tests
  ↓
Progressive production rollout
Senior-level automation habits
  • Keep fast checks early.
  • Make failures actionable.
  • Use the same commands locally and in CI.
  • Prefer reproducible environments.
  • Block deploys on meaningful quality gates.
  • Add observability checks after deployment.

This ties back to System Design Principles: design for failure does not stop at architecture diagrams. It continues into release automation, rollback plans, and feedback loops.

Automate The Path

Senior Testing Review

During code review, you see a pull request that adds a feature but only includes tests that assert private helper methods were called in a specific order.

What is the strongest review comment?

Course Wrap

The senior backend loop

You now have the core map of senior backend engineering:

  1. Model data carefully — from Introduction to Databases.
  2. Design through tradeoffs — from System Design Principles.
  3. Choose boundaries deliberately — from Backend Architecture Patterns.
  4. Use languages and frameworks with intent — from Advanced Languages and Frameworks.
  5. Protect change with tests and automation — from this lesson.

A strong backend engineer can implement features. A senior backend engineer can help a team change complex systems safely.

Your ongoing practice checklist

Before shipping meaningful backend changes, ask:

  • What promise does this code make?
  • Which test proves that promise cheaply?
  • Which boundary could fail in production?
  • Which automated gate catches regressions before users do?
  • If this deploy goes wrong, how do we detect and roll back?

That mindset is the real final project: turning technical knowledge into reliable engineering judgment.

Course Wrap

Open interactive view →