Maintenance

Site is under maintenance — quizzes are still available.

Go to quizzes
Sponsored Reserved space — layout preview until AdSense is connected

Tutorial

Beyond 'It Works on My Machine' — Testing Strategies That Actually Save Your Python Project

Learn practical Python testing strategies that go beyond unit tests, including integration tests, smoke tests, and property-based testing, with real-world examples and CI/CD integration tips.

June 2026 · 6 min read · 1 views · 0 hearts

Beyond "It Works on My Machine" — Testing Strategies That Actually Save Your Python Project

You've written the code. The logic is sound. You run it once, it works, you deploy. Then the edge case hits, the data format shifts, or a dependency updates — and your application quietly breaks. This is the moment most Python developers learn that "it works on my machine" is not a testing strategy.

Let's talk about the testing approaches that separate hobby scripts from production-grade applications.

Unit Tests: The Foundation, Not the Whole House

Unit tests check individual functions or methods in isolation. They're fast, they're precise, and they catch logic errors early. But here's the trap: many developers think 100% unit test coverage means their app is robust.

You need unit tests for: - Mathematical or transformation logic - Edge cases in conditional branches - Functions with clear inputs and outputs

You don't need unit tests for: - Everything that touches a database (that's an integration test) - Complex UI interactions - Third-party API wrappers (mock them instead)

Practical tip: Use pytest and unittest.mock. Avoid over-mocking — if you mock everything, you're testing your mock, not your code.

Integration Tests: Where Real Problems Live

Your database query works in isolation. Your API endpoint works in isolation. But does your endpoint actually return the right data when the database has records? Does it handle a missing connection gracefully?

Integration tests verify that your components work together. They're slower than unit tests but catch the bugs that actually reach production.

A practical integration test example with pytest and pytest-django:

def test_user_creation_flow(db, client):
    response = client.post('/api/users/', {'username': 'test_user'})
    assert response.status_code == 201
    # Now check the database directly
    assert User.objects.count() == 1

Integration tests should cover: - Database interactions (with a test database, not production) - API endpoint chains (create → read → update → delete) - File I/O patterns (upload, process, download)

Smoke Tests: The Safety Net

These are the simplest tests you can write — they just check if your application starts and responds. In a CI/CD pipeline, smoke tests run first. If they fail, don't bother running the full suite.

Minimal smoke test example:

def test_app_starts(client):
    response = client.get('/health/')
    assert response.status_code == 200

Property-Based Testing: Let the Computer Find the Bugs

Here's a technique most Python developers overlook. Instead of writing test cases, you define properties your code should always satisfy, then let a library like hypothesis generate hundreds of edge cases automatically.

Example with a sort function:

from hypothesis import given, strategies as st
import builtins

@given(st.lists(st.integers()))
def test_sort_stability(lst):
    sorted_lst = builtins.sorted(lst)
    assert len(sorted_lst) == len(lst)
    assert all(sorted_lst[i] <= sorted_lst[i+1] for i in range(len(lst)-1))

This single test will try empty lists, lists with one element, lists with duplicates, massive lists — things you'd never think to write manually.

The Testing Pyramid, But Flatter

The traditional testing pyramid says: many unit tests, fewer integration tests, even fewer end-to-end tests. That's good advice, but Python projects often benefit from a flatter structure where integration tests make up a larger share.

Why? Python's dynamic typing means most type-related bugs surface at runtime. Unit tests won't catch that you passed a string where an integer was expected. Integration tests will.

Practical CI/CD Integration

Your tests are only useful if they run automatically. Here's a minimal GitHub Actions configuration:

name: Tests
on: [push]
jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - uses: actions/setup-python@v4
        with:
          python-version: '3.11'
      - run: pip install -r requirements.txt
      - run: pytest --cov=myapp --cov-fail-under=80

That last flag — --cov-fail-under=80 — sets a minimum coverage threshold. The build fails if coverage drops below 80%. This prevents coverage from silently eroding over time.

What Most Tutorials Get Wrong

They teach you how to test, but not what to test. Here's a rule of thumb:

Test the things that break. If a piece of code has never caused a bug, don't write a test for it. If it has caused a bug, write a test that would have caught it. Then add that test to your suite permanently — that's regression testing.

The Cold Truth

Good tests don't make your code bug-free. But they make debugging faster, refactoring safer, and deployment less terrifying. Start with integration tests for your riskiest paths, add unit tests for complex logic, and let property-based testing find what you missed. Your future self, debugging at 2 AM, will thank you.

Comments

Questions, corrections, and tips stay visible for everyone reading this page.

0 in thread

Join the discussion

Shown next to your comment.

Up to 4,000 characters

No comments yet

Be the first to leave a note — it helps the next reader.