Refactoring for Testability: Enhancing Python Code with Dependency Injection and Pytest

ArjanCodes////3 min read

Refactoring for Cleaner Test Design

Writing unit tests for legacy or poorly structured code often feels like a battle against the machine. When a function creates its own dependencies internally, testing that function requires heavy-handed monkey patching and complex mocking strategies. This brittle approach makes tests hard to maintain and even harder to read. The solution isn't just better mocks; it's better code design. By refactoring our functions to be more testable, we simultaneously improve the architecture of our entire application.

Implementing Dependency Injection and Protocols

The most effective way to break tight coupling is . Instead of a function instantiating a inside its body, we pass the processor as an argument. This shift gives the caller—and the test suite—full control over the implementation.

To keep things flexible, we define a using 's typing module. This allows us to use duck-typing to create a mock version of the processor that behaves like the real thing without requiring complex inheritance.

from typing import Protocol

class PaymentProcessor(Protocol):
    def charge(self, card: CreditCard, amount: int) -> None:
        ...

def pay_order(order: Order, processor: PaymentProcessor, card: CreditCard):
    if not order.line_items:
        raise ValueError("Order is empty")
    processor.charge(card, order.total_price)

Streamlining Tests with Pytest Fixtures

When multiple tests require the same setup—like a valid object—redundancy creeps in. fixtures solve this by providing standard, reusable objects to your test functions. We can also make these fixtures "future-proof" by calculating dates dynamically. Hard-coding an expiry date of 2024 might work today, but it ensures your tests will break the moment that year passes.

import pytest
from datetime import date

@pytest.fixture
def card():
    future_year = date.today().year + 2
    return CreditCard(number="4111...", expiry_month=12, expiry_year=future_year)

def test_pay_order_valid(card, processor_mock):
    # The card is automatically injected by pytest
    pay_order(my_order, processor_mock, card)

Handling Sensitive Data and Pure Functions

Hard-coding API keys is a major security risk and a testing headache. Move these to environment variables using . This keeps secrets out of your repository and allows different keys for development, testing, and production.

Finally, simplify your logic by identifying functions that don't need to be tied to a class. A validation utility like the luhn_checksum doesn't need self. Converting it to a standalone pure function makes it trivial to test in isolation without instantiating a heavy processor object. This separation of concerns is the hallmark of professional software development.

Topic DensityMention share of the most discussed topics · 7 mentions across 7 distinct topics
14%· products
14%· concepts
14%· concepts
14%· concepts
14%· libraries
Other topics
29%
End of Article
Source video
Refactoring for Testability: Enhancing Python Code with Dependency Injection and Pytest

How To Write Unit Tests For Existing Python Code // Part 2 of 2

Watch

ArjanCodes // 17:18

On this channel, I post videos about programming and software design to help you take your coding skills to the next level. I'm an entrepreneur and a university lecturer in computer science, with more than 20 years of experience in software development and design. If you're a software developer and you want to improve your development skills, and learn more about programming in general, make sure to subscribe for helpful videos. I post a video here every Friday. If you have any suggestion for a topic you'd like me to cover, just leave a comment on any of my videos and I'll take it under consideration. Thanks for watching!

What they talk about
AI and Agentic Coding News
Who and what they mention most
Python
33.3%5
Python
20.0%3
Python
20.0%3
Pydantic
13.3%2
3 min read0%
3 min read