AlgoInsight

Free lesson · Python Basic to Advance

Why Test

Hard8 min readpython

Why Test

What Testing Is

A test is code that runs other code and checks the result against an expected outcome. Testing is the practice of writing those checks systematically, so that changes to a codebase can be verified automatically rather than by manual inspection.

def add(a, b):
    return a + b

# A simple test
def test_add():
    assert add(2, 3) == 5
    assert add(-1, 1) == 0
    assert add(0, 0) == 0

test_add()
print("all checks passed")
This code shows the essential form of a test. The function under test is add. The test function calls add with known inputs and asserts the result matches expectations. When all assertions hold, the test passes silently. When one fails, it raises an AssertionError with line context.

Tests are a feedback loop. They tell you whether code behaves as intended - now, and after every future change.

What Testing Buys

Tests serve several purposes that compound over time:

A small change in a function you wrote six months ago can break code elsewhere. Without tests, that break may not appear until production. With tests, it appears the moment the change is saved.

The Cost-Benefit Trade

Testing has a real cost - time spent writing tests, time spent maintaining them, code that exists only to verify other code. The benefit compounds over the codebase's lifetime.

For one-off scripts that solve a problem and are discarded, the cost-benefit usually favors no tests. For code that lives, gets read, gets changed, and gets relied on by others - the calculus inverts.

# Throwaway script - probably skip tests
def parse_my_one_csv():
    with open("today.csv") as f:
        return [line.split(",") for line in f]

# Library code - tests are essential
def parse_csv(path, delimiter=",", skip_header=True):
    """Parse a CSV file with configurable options."""
    # ...
This code contrasts contexts. The throwaway script will run once. The library function will be called by other code in different contexts. The library function carries an implicit contract that tests can verify and document. The throwaway script has no future users to protect.

Categories of Tests

Tests come in layers, each verifying different scope:

A common heuristic: many unit tests, fewer integration tests, even fewer end-to-end tests. The pyramid metaphor - broad base of fast unit tests, narrow tip of slow full-system tests - reflects the trade-off between coverage and execution time.

Test-First vs Test-After

Two workflows:

TDD is a discipline that shapes the design: code written to pass an immediate test tends to be small, well-bounded, and verifiable. Test-after is more common in practice and works fine for most code. The choice is largely cultural.

# TDD workflow - example sequence
# 1. Write the test (fails)
def test_double():
    assert double(3) == 6

# 2. Write minimal code (passes)
def double(n):
    return n * 2

# 3. Add a more demanding test
def test_double_handles_negatives():
    assert double(-2) == -4

# 4. Refactor if needed (still passes)
This code shows the TDD cycle. Start with a failing test. Write the minimum code to pass. Add another test demanding more. Refactor when the implementation reveals improvements. The discipline keeps code small and focused on demonstrated requirements rather than imagined ones.

What Makes a Good Test

A few properties distinguish strong tests from weak ones:

# Weak test - vague, dependent on output format
def test_user():
    user = create_user("alice")
    print(user)    # Manual inspection needed

# Strong test - focused, self-validating
def test_create_user_sets_name():
    user = create_user("alice")
    assert user.name == "alice"

def test_create_user_assigns_id():
    user = create_user("alice")
    assert user.id is not None
This code contrasts test quality. The weak version requires reading output to know if it worked. The strong version asserts specific properties - the test passes silently or fails loudly with a clear message. Each focused test has one reason to fail, making debugging much faster.

The AAA Pattern

A common structure for tests - Arrange, Act, Assert:

def test_account_deposit():
    # Arrange - set up the state
    account = Account(balance=100)

    # Act - perform the operation
    account.deposit(50)

    # Assert - verify the outcome
    assert account.balance == 150
This code uses the AAA pattern. Arrange sets up the preconditions. Act performs the operation being tested. Assert verifies the result. The three-phase structure makes tests easy to read and maintain. Blank lines or comments often separate the phases for clarity.

Following AAA makes tests easier to scan. A reader can identify the setup, the action, and the verification at a glance.

Test Failure Messages

Good tests give clear failure messages. Poor tests give cryptic ones:

# Weak failure message
def test_calculation():
    result = compute(10)
    assert result == 42

# When this fails:
# AssertionError

# Strong failure message
def test_calculation():
    result = compute(10)
    assert result == 42, f"compute(10) returned {result}, expected 42"

# When this fails:
# AssertionError: compute(10) returned 37, expected 42
This code shows assertion messages. A plain assert gives no context on failure. Adding a message after the comma includes diagnostic information - the actual value, the expected value, the input that produced it. Topic 23.3 covers pytest's automatic introspection that makes messages clear without manual formatting.

pytest (Topic 23.3) introspects assertions automatically - so the manual message is often unnecessary. With unittest or plain assert, explicit messages help.

When Tests Fail

A failing test should be informative: name describes the behavior, traceback shows the line, assertion shows the values. The goal is for a developer reading the failure to understand the problem in seconds, not minutes.

When a test fails:

The reflex to "make the test green" by relaxing the assertion is a trap. The test exists to enforce behavior; weakening it weakens the protection.

Common Anti-Patterns

A few patterns to avoid:

# Anti-pattern - many unrelated assertions
def test_user():
    user = create_user("alice")
    assert user.name == "alice"
    assert user.id is not None
    assert user.created_at is not None
    user.set_email("alice@example.com")
    assert user.email == "alice@example.com"
    user.activate()
    assert user.is_active

# Better - focused tests, easier to debug
def test_create_user_sets_name(): ...
def test_create_user_assigns_id(): ...
def test_set_email_updates_email(): ...
def test_activate_makes_user_active(): ...
This code contrasts patterns. The first version mixes multiple behaviors into one test - when it fails, the line tells you what broke but not which behavior. The second version separates each behavior. Failures pinpoint exactly which method or property is broken.

The Python Testing Ecosystem

Python has two major testing tools and several supporting libraries:

For new code, pytest is usually the right choice. unittest remains common in older codebases and the standard library itself.

Tests Run Automatically

The point of tests is to run them often - ideally on every change. Mechanisms:

A test suite that runs in seconds gets run often. One that takes ten minutes gets skipped, then ignored, then forgotten. Fast tests are not a nicety - they're a requirement for the practice to work.

Interview Relevance

Interview: Testing fundamentals are expected:

  • What tests are and why they exist
  • Unit vs integration vs end-to-end
  • The AAA pattern
  • What makes a test fast, independent, repeatable
  • unittest vs pytest (most new code uses pytest)
  • Test-driven development as a discipline

A common interview moment: a candidate dismisses tests as "no time for that on this project." The right framing is that tests pay back the cost on the first regression they catch - which usually happens within days.

Another moment: explaining why a test that tests everything in one function is a problem. The right answer involves diagnosability when it fails.

Module 30 covers Python interview preparation.

Common Pitfalls

Warning: Tests that pass without exercising the code. Often happens with empty assertions or mocks that hide reality. Verify the test fails when the code is wrong.

One test per behavior, ignored. Tests that bundle many assertions hide which behavior actually failed.

Tests depending on order. Creates fragility. Each test should run in isolation.

Skipping tests because "the change is small." Most regressions come from small changes.

Testing only the happy path. Edge cases - empty input, large input, error conditions - are where bugs live.

Tests that take too long to run. Slow tests get skipped. Keep unit tests in milliseconds.

Treating tests as second-class code. Test code lives as long as production code; it needs the same care.

Try It

Exercise: Practice with the testing mindset:

  1. Pick a function from your code. Write three tests for it - one happy path, two edge cases.
  2. Run your tests. Modify the function to break it deliberately. Verify the tests catch the break.
  3. Refactor a test that bundles multiple assertions into multiple focused tests.
  4. Pair a test name with the behavior it verifies. Make the name describe the contract.
  5. Time your test suite. Identify the slowest tests and consider why they're slow.
  6. For a piece of code without tests, write the test you wish existed before changing it.

Back to Python Basic to Advance