Chapter 11: Testing Your Code
Testing proves your code works. Catch bugs before users do.
Installing pytest
pytest is a third-party package. Install with pip:
python -m pip install --user pytest
Verify installation:
pytest --version
Testing a Function
The Function to Test
# network.py
def get_connection_string(host, port, protocol='tcp'):
"""Return formatted connection string."""
return f"{protocol}://{host}:{port}"
Writing a Test
# test_network.py
from network import get_connection_string
def test_basic_connection():
"""Test basic connection string."""
result = get_connection_string('10.0.1.10', 443)
assert result == 'tcp://10.0.1.10:443'
Key rules:
-
Filename starts with
test_ -
Function name starts with
test_ -
Use
assertto verify results
Running Tests
pytest
Output for passing test:
========================= test session starts ========================= collected 1 item test_network.py . [100%] ========================== 1 passed in 0.01s ==========================
Single dot = one passing test.
A Failing Test
If we break the function:
========================= test session starts =========================
collected 1 item
test_network.py F [100%]
============================== FAILURES ===============================
________________________ test_basic_connection ________________________
def test_basic_connection():
"""Test basic connection string."""
result = get_connection_string('10.0.1.10', 443)
> assert result == 'tcp://10.0.1.10:443'
E AssertionError: assert 'tcp:10.0.1.10:443' == 'tcp://10.0.1.10:443'
test_network.py:6: AssertionError
========================== 1 failed in 0.04s ==========================
The F indicates failure. Output shows exactly what failed and why.
Responding to Failures
When a test fails:
-
Don’t change the test (unless it’s wrong)
-
Fix the code that broke
-
Run tests again
Tests define correct behavior. Fix code to match.
Multiple Tests
# test_network.py
from network import get_connection_string
def test_basic_connection():
"""Test with default protocol."""
result = get_connection_string('10.0.1.10', 443)
assert result == 'tcp://10.0.1.10:443'
def test_custom_protocol():
"""Test with explicit protocol."""
result = get_connection_string('10.0.1.10', 443, 'https')
assert result == 'https://10.0.1.10:443'
def test_different_port():
"""Test different port number."""
result = get_connection_string('db-01', 5432)
assert result == 'tcp://db-01:5432'
test_network.py ... [100%] ========================== 3 passed in 0.01s ==========================
Three dots = three passing tests.
Assertion Types
# Equality
assert result == expected
assert result != wrong_value
# Boolean
assert is_valid
assert not is_error
# Membership
assert 'error' in log_message
assert server not in blocked_list
# Type checking
assert isinstance(result, dict)
Testing a Class
# server.py
class Server:
def __init__(self, hostname, ip):
self.hostname = hostname
self.ip = ip
self.status = 'stopped'
def start(self):
self.status = 'running'
def stop(self):
self.status = 'stopped'
# test_server.py
from server import Server
def test_initial_status():
"""New servers start stopped."""
srv = Server('web-01', '10.0.1.10')
assert srv.status == 'stopped'
def test_start():
"""Start changes status to running."""
srv = Server('web-01', '10.0.1.10')
srv.start()
assert srv.status == 'running'
def test_stop():
"""Stop changes status to stopped."""
srv = Server('web-01', '10.0.1.10')
srv.start()
srv.stop()
assert srv.status == 'stopped'
Fixtures
Fixtures provide shared setup. Avoid repeating code.
Without Fixture
def test_start():
srv = Server('web-01', '10.0.1.10') # repeated
srv.start()
assert srv.status == 'running'
def test_stop():
srv = Server('web-01', '10.0.1.10') # repeated
srv.start()
srv.stop()
assert srv.status == 'stopped'
With Fixture
import pytest
from server import Server
@pytest.fixture
def web_server():
"""Provide a Server instance for tests."""
return Server('web-01', '10.0.1.10')
def test_initial_status(web_server):
"""New servers start stopped."""
assert web_server.status == 'stopped'
def test_start(web_server):
"""Start changes status to running."""
web_server.start()
assert web_server.status == 'running'
def test_stop(web_server):
"""Stop changes status to stopped."""
web_server.start()
web_server.stop()
assert web_server.status == 'stopped'
-
@pytest.fixturedecorator marks the function -
Test functions receive fixture by parameter name
-
Each test gets a fresh instance
Test Organization
Project Structure
project/
├── network.py
├── server.py
└── tests/
├── test_network.py
└── test_server.py
Run all tests:
pytest
Run specific file:
pytest tests/test_server.py
Run specific test:
pytest tests/test_server.py::test_start
Naming Conventions
-
Test files:
test_*.pyor*_test.py -
Test functions:
test_* -
Test classes:
Test*
Descriptive Names
# Good - describes what's being tested
def test_connection_string_with_default_protocol():
def test_server_starts_in_stopped_state():
def test_invalid_port_raises_error():
# Bad - vague
def test_connection():
def test_server():
def test_port():
Testing Patterns
Testing for Exceptions
import pytest
def test_invalid_port_raises_error():
with pytest.raises(ValueError):
create_connection('host', -1)
Parameterized Tests
Test multiple inputs with one function:
import pytest
@pytest.mark.parametrize("host,port,expected", [
('web-01', 80, 'tcp://web-01:80'),
('db-01', 5432, 'tcp://db-01:5432'),
('cache', 6379, 'tcp://cache:6379'),
])
def test_connection_strings(host, port, expected):
result = get_connection_string(host, port)
assert result == expected
When to Test
-
Test critical behaviors first
-
Test edge cases (empty input, zero, negative)
-
Test error conditions
-
Add tests when fixing bugs (prevent regression)
You don’t need 100% coverage. Test what matters.
Quick Reference
| Command | Purpose |
|---|---|
|
Run all tests |
|
Run tests in file |
|
Verbose output |
|
Stop on first failure |
|
Shorter tracebacks |
| Pattern | Code |
|---|---|
Assert equal |
|
Assert true |
|
Assert in |
|
Fixture |
|
Expect exception |
|
Exercises
11-1. City Function
Write format_city(city, country) returning "City, Country".
Write tests for it.
11-2. Server Class
Test your Server class from Chapter 9. Test initial state and methods.
11-3. Fixture Practice
Create a fixture that provides a configured server. Use it in 3 tests.
11-4. Edge Cases
Write tests for: empty strings, very long strings, special characters.
11-5. Error Handling
Write a function that raises ValueError for invalid input. Test that it raises correctly.
Summary
-
pytestdiscovers and runs test files/functions starting withtest_ -
assertverifies expected outcomes -
Failing tests show exactly what went wrong
-
Fix code, not tests, when tests fail
-
Fixtures provide shared setup with
@pytest.fixture -
Run specific tests:
pytest file.py::test_name -
Test critical behaviors and edge cases
Part I complete. Next: Building projects.