How to Write Easily Testable Python Code: A Step-by-Step Guide

By

Introduction

Writing unit tests is one of the most empowering skills you can develop as a programmer. It transforms your ability to build reliable, maintainable software. But tests are only as good as the code they test. The secret to effortless testing lies in making your code prime testable—a term for code that has no side effects and is deterministic. This guide will show you how to identify, isolate, and write such code, making testing a breeze. By the end, you'll have a repeatable process to refactor any codebase into a test-friendly structure.

How to Write Easily Testable Python Code: A Step-by-Step Guide
Source: dev.to

What You Need

  • A working Python environment (version 3.6+)
  • A codebase you want to test (even a small script works)
  • A testing framework (e.g., pytest or unittest)
  • Familiarity with basic Python functions and classes
  • Willingness to refactor—sometimes heavily

Step-by-Step Process

Step 1: Identify Code That Is Hard to Test

Before you can improve testability, you need to spot the troublemakers. Look for functions or methods that:

  • Have side effects: modify a database, write to a file, change a global variable, send an email, or open a garage door.
  • Are non-deterministic: return different results for the same inputs—like fetching the current time, a random number, or an API response.

For example, a function that logs in a user, writes to a database, and returns a session token is both side-effect‑laden and non-deterministic. It's hard to test without mocking everything.

Step 2: Extract the Pure Core

Once you've identified a hard-to-test function, look for any logic that can be separated into a pure function—one with no side effects and deterministic behavior. This is the "prime testable" part. Ask yourself: What calculation or transformation is happening inside this function that doesn't depend on external state?

Suppose you have a function that validates a password against a stored hash. The hashing algorithm itself is deterministic: given the same password and salt, it always produces the same hash. Extract that hashing step into its own function. Similarly, string parsing, mathematical computations, and data transformations are often pure.

Step 3: Refactor to Isolate Prime‑Testable Components

Now refactor your code so that the pure part lives in its own function or method. The remaining code (which must have side effects or be non-deterministic) should call that pure function. This creates a clean separation. For example:

# Before: hard to test
def create_user(username, plain_password):
    # side effect: write to DB
    db.save(username, plain_password)
    return True

# After: prime testable core extracted
def _hash_password(plain_password):
    import hashlib
    # deterministic, no side effects
    return hashlib.sha256(plain_password.encode()).hexdigest()

def create_user(username, plain_password):
    hashed = _hash_password(plain_password)
    # side effect kept separate
    db.save(username, hashed)
    return True

Now _hash_password is prime testable. You can test it without mocking anything.

How to Write Easily Testable Python Code: A Step-by-Step Guide
Source: dev.to

Step 4: Write Tests for the Prime‑Testable Functions

With the pure function isolated, writing tests becomes straightforward. Use your testing framework to call the function with known inputs and assert the expected outputs. Because there are no side effects, you don't need mocks or fixtures. For example:

def test_hash_password():
    result = _hash_password("supersecret")
    expected = "2f77668a9dfbf8d5848b9e3a..."  # known hash
    assert result == expected

Test edge cases like empty strings, special characters, and very long inputs. Since the function is deterministic, these tests are reliable and fast.

Step 5: Integrate Back and Test the Outer Function

Now that the core logic is tested, you can write integration tests for the outer function that still has side effects. Use mocks or stubs for the database calls. Because the pure part is already verified, your integration tests can focus on whether the side effect happens correctly (e.g., the right data is saved).

Step 6: Repeat for Every Complex Module

Go through your codebase and apply this pattern to every module that mixes pure logic with impure operations. The more code you move into prime‑testable functions, the easier your tests become. Over time, you'll develop an instinct for spotting what can be extracted.

Conclusion and Tips

Making your code prime testable is not always possible—some functions genuinely need side effects. But you can almost always reduce the amount of impure code by pushing the pure parts to the edges.

  • Start small: Pick one function that's giving you testing headaches and refactor it. You'll see immediate benefits.
  • Use dependency injection: For unavoidable side effects, pass in dependencies (like a database handler) so you can swap them with mocks during tests.
  • Keep functions focused: A function that does one thing (especially a pure transformation) is easier to test than a monolithic one.
  • Embrace functional programming techniques: Think in terms of input → output. Avoid hidden state.

Remember, every prime‑testable function you create is a victory. You'll write more tests, your confidence will grow, and your code will become more robust. Start applying these steps today, and watch your testing experience transform.

Related Articles

Recommended

Discover More

10 Key Facts Behind Apple's $250 Million Siri SettlementBuilding Unified Spatial Atlases: A Step-by-Step Guide to Integrating Fragmented Cell MapsDrone Traffic Control: Cornell Students Innovate with NASA SupportDocker Container Security Best PracticesHow to Immerse Yourself in 'Go with the Clouds, North-by-Northwest' Before the Anime Debuts