Dependency Injection & Testing

Reference: The Clean Code Talks - Don’t Look For Things!

Testing Challenges

Complex constructors with direct instantiation of objects or reliance on static initialization and singletons make testing difficult.

class Document:
    def __init__(self):
        self.html_client = HTMLClient()  # Direct instantiation

    def load_content(self, url):
        return self.html_client.fetch(url)

# Testing this requires setting up the HTMLClient and its dependencies
More details

Dependency Injection vs Direct Instantiation

Dependency injection enhances testability by allowing flexible substitution of dependencies.

class HTMLClient:
    def fetch(self, url): # Complex logic to fetch data from a URL
        return f"Fetched content from {url}"

class Document:
    def __init__(self, html_client):
        self.html_client = html_client # Dependency injected

    def load_content(self, url):
        return self.html_client.fetch(url)

# With dependency injection, testing becomes easier
# Mock or substitute HTMLClient can be injected for testing purposes
More details
class MockHTMLClient:
    def fetch(self, url):
        return "Mocked content"

# Create a mock HTMLClient for testing & inject mock_client into Document for testing
mock_client = MockHTMLClient()
test_document = Document(mock_client)
assert test_document.load_content("http://test.com") == "Mocked content"

Exact Dependencies in Constructors

Constructors should explicitly ask for their dependencies, promoting clear and maintainable code.

class Document:
    def content(self):
        return "Document content"

class Printer:
    def __init__(self, document):
        self.document = document  # Direct dependency

    def print(self):
        return f"Printing: {self.document.content()}"

# Clear and testable, as the dependency on Document is explicit
More details
class MockDocument:
    def content(self):
        return "Mock content"

# Inject a MockDocument into Printer for testing
mock_document = MockDocument()
printer = Printer(mock_document)
assert printer.print() == "Printing: Mock content"

Law of Demeter

Following this law reduces coupling by limiting an object’s interactions to its immediate dependencies.

# In practice:
class Door:
    def open(self):
        return "Door opened"

class House:
    def __init__(self, door):
        self.door = door # Interact only with the door

    def open_front_door(self):
        return self.door.open()
# The House class does not need to know about the details behind the Door class
More details
class Door:
    def __init__(self):
        self.lock_status = "Locked"

    def unlock(self):
        self.lock_status = "Unlocked"

    def open(self):
        if self.lock_status == "Unlocked":
            return "Door opened"
        return "Door is locked"

class House:
    def __init__(self, door):
        self.door = door

    def open_front_door(self):
        return self.door.open()

# Later, if changes are made to Door's internal structure
# House class remains unaffected as long as the 'open' method's interface is stable.
class MockDoor:
    def open(self):
        return "Mock door opened"

# Testing House with a MockDoor
mock_door = MockDoor()
house = House(mock_door)
assert house.open_front_door() == "Mock door opened"