Please use a larger screen

This presentation is designed for desktop or projector displays.

← Back to curriculum

Python Object Oriented Programming

Unit Testing & TDD

Course 9

test_runner.py PASSED PASSED PASSED 3 passed in 0.01s

to navigate

Course Curriculum

Where we are in the journey

Quiz Time! (Lecture 07)

Which SOLID principle does this code violate?

class Bird:
    def fly(self): ...

class Penguin(Bird):
    def fly(self):
        raise NotImplementedError("Penguins can't fly")

A) Single Responsibility

B) Liskov Substitution

C) Interface Segregation

D) Dependency Inversion

Penguin breaks its parent's contract — code expecting Bird crashes on Penguin. Subtypes must be substitutable for their base type.

Recap: Lecture 07

SOLID: Liskov Substitution

  • 🔒 Subtypes must be substitutable for their base type
  • 🐛 A subclass shouldn't break promises the parent makes
  • 🚫 No surprises: no new exceptions, stricter args, weaker return guarantees
  • 💡 Split hierarchies when the "is-a" relationship leaks
class Bird:
    ...

class FlyingBird(Bird):
    def fly(self): ...

class Sparrow(FlyingBird):
    ...

class Penguin(Bird):
    ...

Quiz Time! (Lecture 08)

What problem does the Singleton pattern solve?

A) Creating many objects fast

B) Guaranteeing exactly one instance with global access

C) Hiding implementation details

D) Adding behavior at runtime

Singleton ensures a class has only one instance and provides a global access point — used for shared resources like config, logging, and database connections.

Recap: Lecture 08

Design Patterns: Singleton

  • 🔸 One class, one instance, one global access point
  • 🔁 Override __new__ to control creation
  • 🗃️ Database connections, configuration, loggers
  • ⚠️ Avoid for anything else — global state is a code smell
class Config:
    _instance = None

    def __new__(cls):
        if cls._instance is None:
            cls._instance = super().__new__(cls)
        return cls._instance

Quiz Time! (Lecture 06)

What does the else clause do in a try/except?

try:
    value = int(user_input)
except ValueError:
    print("Not a number")
else:
    print(f"Got {value}")

A) Runs when an exception is caught

B) Runs only if no exception was raised

C) Always runs, like finally

D) Runs only if try raises

The else block runs only when the try completed without raising. Use it for code that should run only on the "happy path".

Recap: Lecture 06

Exception Handling: try / except / else

  • try — code that may raise
  • except — catches a specific exception
  • else — runs only when try succeeded
  • finally — always runs, for cleanup
try:
    data = parse(raw)
except ValueError as e:
    log.error(e)
else:
    save(data)
finally:
    cleanup()

Agenda for Today

Why Testing Matters

"Code without tests is broken by design." — Jacob Kaplan-Moss

🧪 Testing in Real Life

🚨

Smoke Detector

Tests fire safety before there's a fire. Cheap sensor → early warning → saved home.

🏭

Factory QC

Every product checked before shipping. Defects caught on the line, not by customers.

🚗

Crash Test Dummy

Simulate disaster, fix the design. You don't fix the dummy — you fix the car.

Good engineering always tests before shipping. Software is no exception.

📜 A Brief History

The current meaning of software testing started to develop in the early 1950s, when Joseph Juran — the father of software testing — formalized it as a separate discipline from debugging.
  • 📚 Before then, "testing" meant "does the program run at all?"
  • 🔎 Juran separated verification (matches spec) from debugging (finds defects)
  • 🛠️ Today it's a full discipline — unit, integration, E2E, performance, security...
Joseph Juran

Joseph Juran

1904 – 2008

Testing in the SDLC

📋
Requirements
📅
Planning
🎨
Design
💻
Development
🧪
Testing
our focus
🚀
Deployment
🛠️
Maintenance

Testing is woven through every phase — not a single step.

Software testing is the process of verifying and validating software to ensure it meets specified requirements.

⚖️ Verification vs Validation

Verification

"Are we building the product right?"

  • 🔍 Process focus
  • 📅 Throughout the lifecycle
  • 📋 Reviews, inspections, unit tests
  • 👨‍💻 Developers & QA

Validation

"Are we building the right product?"

  • 📦 Product focus
  • 🏁 Towards the end
  • ✅ User, system, beta testing
  • 👥 End users
Verification = follows the spec. Validation = solves the user's problem.

Why Test? Six reasons

Testing pays for itself — many times over

🔎 Why Test? (1/2)

🐛

Catch Bugs Early

Bugs caught during development are cheaper to fix — before they reach users.

💰

Save Cost & Time

Bugs in production cost 10-100x more to fix than bugs during coding.

🔧

Refactor Safely

Change code without fear of breaking things. Green tests = freedom.

🔎 Why Test? (2/2)

😄

Happy Users

Fewer crashes → less churn → more loyal customers.

🏆

Brand Reputation

Buggy software damages trust. Reliable software builds it.

🔒

Security

Exposes vulnerabilities before attackers do.

💰 The Cost-of-Bug Curve

Cost Phase Requirements 1x Design ~5x Code ~10x Test ~30x Production 100x+

The earlier you catch a bug, the cheaper it is.

Types of Testing

Functional vs non-functional — and the pyramid

⚖️ Functional vs Non-Functional

⚙️ Functional

What the system does — features, behavior, business logic.

"Can the user place an order?"

⚡ Non-Functional

How the system does it — performance, security, usability.

"Can it handle 10,000 users at once?"

Non-Functional Examples

Performance

Speed and response time under normal load.

📊

Load

Behavior under expected peak traffic.

🔥

Stress

Breaking points — what happens past the limit?

👥

Usability

Can users actually accomplish their goals?

🔒

Security

Auth, injection, data leaks, attacks.

🌐

Compatibility

Browsers, OSes, devices, API versions.

Functional Examples

Unit

One function or class in isolation.

🔗

Integration

Multiple components working together.

Acceptance

Meets business requirements & user stories.

Regression

Old features still work after a change.

🎬

End-to-End

The whole system from user input to output.

The Test Pyramid

Unit Tests Integration E2E slow, few, expensive fast, many, cheap

Pyramid, not ice cream cone — most tests should be unit tests.

Unit Testing

The foundation of the pyramid

🧩 What is a "unit"?

If you're testing more than one class, it's no longer a unit test — that's integration.

🔎 Why Unit Test?

📋 The AAA Pattern

Arrange — Act — Assert. Three lines, three responsibilities.

def test_rectangle_area():
    rect = Rectangle(10, 20)         # Arrange
    area = rect.area()               # Act
    assert area == 200               # Assert
Read top-to-bottom. Arrange the inputs, act on the subject, assert the outcome.

📋 AAA = Given / When / Then

Same idea, different vocabulary. BDD (Behavior-Driven Development) uses Given / When / Then — it maps 1:1 onto AAA.

📋 AAA (unit tests)

def test_withdraw_reduces_balance():
    # Arrange
    account = Account(100)

    # Act
    account.withdraw(30)

    # Assert
    assert account.balance == 70

📝 Given / When / Then (BDD)

def test_withdraw_reduces_balance():
    # Given an account with balance 100
    account = Account(100)

    # When I withdraw 30
    account.withdraw(30)

    # Then balance should be 70
    assert account.balance == 70
Same structure, different phrasing. AAA speaks about code mechanics; Given/When/Then speaks about business behavior. Pick whichever reads clearer to your team.

The 4 Phases of Writing Tests

Example 1: Testing a function

The function (math_utils.py):

def add(x, y):
    return x + y

The test (test_math_utils.py):

import unittest
from math_utils import add

class TestAdd(unittest.TestCase):
    def test_add_positive(self):
        self.assertEqual(add(2, 3), 5)

    def test_add_zero(self):
        self.assertEqual(add(0, 0), 0)

Run it:

$ python -m unittest test_math_utils.py
..
----------------------------------------
Ran 2 tests in 0.001s

OK

Example 2: Testing a class

The class under test:

class Rectangle:
    def __init__(self, width, height):
        self.width = width
        self.height = height

    def area(self):
        return self.width * self.height

The test:

class TestRectangle(unittest.TestCase):
    def test_area(self):
        rect = Rectangle(10, 20)
        self.assertEqual(rect.area(), 200)

    def test_zero_dimensions(self):
        rect = Rectangle(0, 5)
        self.assertEqual(rect.area(), 0)

Output:

..
Ran 2 tests in 0.001s
OK

Example 3: Testing for exceptions

The function that raises:

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

Asserting the exception:

class TestDivide(unittest.TestCase):
    def test_divide_by_zero(self):
        with self.assertRaises(ValueError):
            divide(10, 0)

    def test_divide_normal(self):
        self.assertEqual(divide(10, 2), 5)

Output:

..
Ran 2 tests in 0.001s
OK

pytest vs unittest

Two ways to write tests in Python

unittest — the standard library

🔗 docs.python.org/3/library/unittest.html

pytest — the community favorite

🔗 docs.pytest.org

Example 4: Same Test, Two Styles

unittest:

import unittest
from rectangle import Rectangle

class TestRectangle(unittest.TestCase):
    def test_area(self):
        rect = Rectangle(10, 20)
        self.assertEqual(rect.area(), 200)

pytest:

from rectangle import Rectangle

def test_area():
    rect = Rectangle(10, 20)
    assert rect.area() == 200

✅ Same behavior, half the code.

Example 5: pytest with Multiple Cases

The function:

def is_prime(n):
    if n < 2:
        return False
    for i in range(2, int(n ** 0.5) + 1):
        if n % i == 0:
            return False
    return True

pytest test functions:

def test_primes():
    assert is_prime(2)
    assert is_prime(13)

def test_non_primes():
    assert not is_prime(4)
    assert not is_prime(9)

def test_negatives():
    assert not is_prime(-5)

Output:

===== 3 passed in 0.02s =====

Parametrized Tests

Without parametrize — repetitive:

def test_is_prime_2():
    assert is_prime(2) == True

def test_is_prime_4():
    assert is_prime(4) == False

def test_is_prime_13():
    assert is_prime(13) == True

With parametrize — DRY:

import pytest

@pytest.mark.parametrize("n,expected", [
    (2, True),
    (4, False),
    (13, True),
])
def test_is_prime(n, expected):
    assert is_prime(n) == expected

Output:

test_is_prime[2-True]  PASSED
test_is_prime[4-False] PASSED
test_is_prime[13-True] PASSED
===== 3 passed in 0.02s =====

pytest Fixtures

The problem — duplicated setup:

def test_area():
    rect = Rectangle(10, 20)
    assert rect.area() == 200

def test_perimeter():
    rect = Rectangle(10, 20)
    assert rect.perimeter() == 60

Fixture extracts setup:

import pytest

@pytest.fixture
def rect():
    return Rectangle(10, 20)

def test_area(rect):
    assert rect.area() == 200

def test_perimeter(rect):
    assert rect.perimeter() == 60

Why fixtures?

  • 🔁 Reusable setup, scoped to function / class / module / session
  • 🛠️ Can yield for teardown after the test

🚨 Handling Failed Tests

$ pytest tests/

tests/test_math.py .F

=============== FAILURES ===============
_______ test_divide_positive _______

    def test_divide_positive():
>       assert divide(1, 2) == 0.6
E       assert 0.5 == 0.6
E         +  where 0.5 = divide(1, 2)

tests/test_math.py:12: AssertionError
========== 1 failed, 1 passed in 0.02s ==========

pytest tells you exactly what failed and why.

🐛 5-Step Debugging Workflow

Mocking

"Don't test the world. Test your code."

🎭 What is Mocking?

  • ⚡ Don't deploy Kafka, connect to AWS Postgres, or call a real bank API to run a test
  • 🎭 Replace external dependencies with stand-ins that live entirely in memory
  • 🎯 Your test runs in milliseconds — no network, no containers, no cloud
  • 🎮 You control what the mock returns — including failures you want to simulate
Your test is checking your code's logic, not whether AWS is up.
❌ Without mocks test AWS DB Postgres/RDS Bank API 3rd party Kafka message bus SMTP email slow · flaky · costly needs network ✅ With mocks test mock DB mock Bank mock Kafka mock SMTP all in-memory no network fast · deterministic zero infrastructure

Why Mock?

🎯 Isolation

Test one thing, not the whole ecosystem.

🎯 Control

Simulate errors, timeouts, edge cases on demand.

⚡ Speed

No real DB or network calls — milliseconds, not seconds.

🧪 Determinism

No flakiness from external systems.

Example 6: pytest-mock

The function under test (user_service.py):

def get_user_by_id(db, user_id):
    user = db.get_user(user_id)
    if user is None:
        raise ValueError(f"User {user_id} not found")
    return user

Test with mocker:

def test_get_user_success(mocker):
    fake_db = mocker.Mock()
    fake_db.get_user.return_value = {"id": 1, "name": "Alice"}

    user = get_user_by_id(fake_db, 1)

    assert user["name"] == "Alice"
    fake_db.get_user.assert_called_once_with(1)

Test the error path:

def test_get_user_not_found(mocker):
    fake_db = mocker.Mock()
    fake_db.get_user.return_value = None

    with pytest.raises(ValueError):
        get_user_by_id(fake_db, 99)

F.I.R.S.T Principles

Five qualities every good test should have.

⭐ Best Practices

Test-Driven Development

Red. Green. Refactor.

What is TDD?

TDD is a development practice where you write tests before the code they test.

  • 🎯 Tests define the design
  • 📝 Production code exists only to make tests pass
  • 🔁 Continuous feedback loop, measured in minutes
It flips the order — tests first, code second.

The TDD Cycle

RED failing test GREEN make it pass REFACTOR clean up

Three steps. Endless cycles.

🔴 Phase 1: RED

Write a failing test first.

  • ✍️ Write a new test for behavior that doesn't exist yet
  • ▶️ Run it — confirm it FAILS for the right reason
  • 🎯 The failing test defines "done"
  • 🚫 Don't write production code yet
A test that never fails doesn't prove your code works.
RED failing test GREEN make it pass REFACTOR clean up

🟢 Phase 2: GREEN

Make the failing test pass — fast and ugly is fine.

  • 💚 Write the minimum code to make the test pass
  • ❌ No clever optimizations, no extras
  • 🎯 "The simplest thing that could possibly work"
  • ✅ Run the suite — all green
Ugly code is fine here. We'll clean it up in the next phase.
RED failing test GREEN make it pass REFACTOR clean up

🔵 Phase 3: REFACTOR

Improve the code without changing behavior.

  • 🔧 Improve the code — without changing behavior
  • 🛡️ Tests are your safety net
  • ✨ Remove duplication, improve naming, simplify structure
  • 🔁 Run the tests often — stay green at every step
Refactoring without tests is just hope.
RED failing test GREEN make it pass REFACTOR clean up

🎬 Live Coding

Let's build a Car class

Three requirements — three full TDD cycles

Requirements

🔴 RED → 🟢 GREEN → 🔵 REFACTOR  —  one requirement at a time

TDD Walkthrough: Car Class

🔴 RED — failing tests first:

def test_car_starts_at_zero():
    assert Car().speed == 0

def test_negative_raises():
    with pytest.raises(ValueError):
        Car().set_speed(-10)

🟢 GREEN — minimum code:

class Car:
    def __init__(self):
        self.speed = 0

    def set_speed(self, value):
        if value < 0: raise ValueError("negative")
        self.speed = value

🔵 REFACTOR — use a property:

    @property
    def speed(self) -> int:
        return self._speed

    @speed.setter
    def speed(self, value: int) -> None:
        if value < 0:
            raise ValueError("Speed cannot be negative")
        self._speed = value

TDD — Pros & Cons

✅ Pros

  • You write only the code that's needed
  • Naturally high test coverage
  • Tests document the intended interface
  • Less time debugging
  • Easier to refactor with confidence
  • Forces you to think about design FIRST

❌ Cons

  • Slower upfront — tests take time to write
  • Tests must change when requirements change
  • Steep learning curve at first
  • Hard to apply to UI / exploratory code

When to Use TDD / When Not

✅ Use it for

  • Business logic & domain rules
  • Libraries & well-defined APIs
  • Bug fixes — write the test first, reproducing the bug
  • Anything with a clear input/output contract

❌ Skip it for

  • Spike / prototype code
  • Quick UI tweaks
  • Throwaway scripts
  • Code where the interface isn't clear yet

🤖 AI & Testing

Copilot, Claude, Cursor — should you let them write your tests?

🤖 Where AI Helps with Tests

✅ Great use cases

  • Boilerplate — setup, fixtures, arrange blocks
  • 🔍 Edge-case brainstorming — "what inputs break this?"
  • 🔁 Refactoring tests — unittest → pytest, adding parametrize
  • 📝 Naming tests — descriptive, consistent patterns
  • 🎭 Mock scaffolding — setting up return values, call assertions

⚠️ Where AI struggles

  • 🧠 Knowing what to test — that's your domain knowledge
  • 🔴 TDD's RED phase — you must own the spec first
  • 🔗 Cross-file business logic — needs context AI may not have
  • 🔒 Critical / security paths — stakes too high for "looks right"

🤖 AI Testing: Pros & Cons

✅ Pros

  • 🚀 10x faster to write first-draft tests
  • 💡 Surfaces cases you wouldn't think of
  • 📚 Writes consistent, readable test code
  • 📄 Good at translating tests between frameworks
  • 🎯 Lowers the barrier to writing any tests at all

❌ Cons

  • 🎭 Tautological tests — pin implementation, can't catch bugs
  • 🤦 Hallucinated assertions that look right but test nothing
  • 🔗 Over-mocked tests that break on harmless refactors
  • 🕶 False confidence — green doesn't mean correct
  • 🚫 Reinforces bad habits if you don't know the good ones
The rule: AI writes tests — you own them. Every AI-generated test needs a human-reviewed "does this actually test something?" pass.

Key Takeaways

Tests aren't extra work — they're the only proof your code does what you think it does.

Next Up

Design Patterns (Part 2)

Strategy Decorator Factory Facade Iterator

Strategy, Decorator, Factory Method, Facade — and more!