Skip to main content

Seamful is a dependency injection library for Python.

It aims to facilitate writing integration tests that replace the minimum amount of dependencies with test doubles. It incentivizes using working test doubles as opposed inspectable mocks.

Seamful intervenes by instantiating the objects and their dependencies. While typically one would manually instantiate these:

my_service = MyService(MyRepository(...))
my_service.do_something()

If instantiation happens "close" to the actual working code, it is hard to replace the dependencies with test_doubles.

Seamful introduces a way of declaring which are these classes that are meant to be instantiated, and recipes for how to build them. Moreover it provides a way of configuring those recipes so that, for example, a test environment can depart from the production configuration just for a few of those classes.

For declaring which classes are meant to be instantiated, Seamful uses a concept called "Module". A module is a class which contains a list of resources, which are the classes that are meant to be instantiated. For example:

from seamful import Module, Resource


class PaymentsModule(Module):
    service = Resource(PaymentsService)
    repository = Resource(PaymentsRepository)

This states that the PaymentsModule has two resources: service and repository. It also states that they ought to be instances of PaymentsService and PaymentsRepository respectively, but it doesn't say how to build them. Since we want to be able to build them in more than one way, we want to put those recipes in a different declaration, which we call a Provider.

A provider is a class that's bound to a specific Module and specifiies builder methods for all the resources in that Module. For example:

class PaymentsProvider(Provider, module=PaymentsModule):

    def get_repository(self) -> PaymentsRepository:
        return PaymentsRepository(...)

    def get_service(self, repository: PaymentsModule.repository) -> PaymentsService:
        return PaymentsService(repository)

While the first method seems straightforward, the second is more surprising. Instead of get_service calling self.get_repository(), it just "asks" for the resource it wants as a function parameter. This are pretty much the main rules for defining a provider method:

  • must be called get_<name_of_the_resource>
  • doesn't use the self parameter
  • if it needs an instance of another resource, from it's module or another module, it requests it as a parameter.
  • annotates its parameter types and return type.
  • returns an instance of the resource it is meant to build.

Also, declaring a provider also need to follow a few rules:

  • it must inherit from Provider
  • it must state which module it provides for via the module= parameter
  • it must have a method for each resource in the module, with the name get_<name_of_the_resource>

Once you have at least one Module and one Provider, you can wire them together in a Application. A application is the entry point for both registering modules and providers and for finally requesting module resources.

They're built as follows:

from seamful import Application

application = Application.empty()
application.install_module(PaymentsModule, PaymentsProvider)
application.ready()

Once a application installed some modules and providers, it can become ready for providing resources. application.ready() checks that the module resources can be built by following the dependency graphs of the providers, which usually become more complex than the example above.

The application can be built and configured as a pubilc, global variable in your application. Then, in the entry points of your application you can import that application and ask for any of the registered modules resources. For example:

from .application import application

class Main:
    def __init__(self):
        self.service = application.provide(PaymentsModule.service)

    def run(self):
        service.do_something()

You can actually use the application anywhere in your application, but it's recommended to use it only in the entry points. Every class that explicitly uses a application will be harder to use in a different use case.

This application can run as-is in production, but it can also be used in integration tests, where the application can be configured to provide a different implementation of some of those resources. Let's imagine that the PaymentsRepository uses a database that we wish not to use in our integration tests. We can create a test_double:

class InMemoryPaymentsRepository(PaymentsRepository):
    def __init__(self):
        self.payments = []

    def save_payment(self, payment: Payment) -> int:
        self.payments.append(payment)
        return len(self.payments) - 1

    def get_payment(self, id: int) -> Payment:
        return self.payments[id]

    def list_payments(self) -> Iterable[Payment]:
        return self._payments[:]

And then we can configure the application to provide that implementation instead of the real one:

import unittest
from .application import application


class PaymentProviderForTests(PaymentsProvider):
    def get_repository(self) -> PaymentsRepository:
        return InMemoryPaymentsRepository()


class TestPayments(unittest.TestCase):
    def setUp(self):
        application.tamper()
        application.install_provider(PaymentProviderForTests)
        application.ready()

    def tearDown():
        application.restore()

    def test_payment_processes_successfully(self):
        service = application.provide(PaymentsModule.service)
        service.process_new_payment(...)
        self.assertEqual(1, len(service.repository.list_payments()))

So far this seems like a lot of boilerplate for something that could be done with a few lines of code and Mocks. The expectation is that as your application grows bigger, the impact of this boilerplate will be less than manually instantiating the object graph manually.

Also, the ability to override only those dependencies that ought to be different in tests ensures that the code being tested is as close as possible as the real application.

Finally, even the outermost part of the application could be tested in this setup. For example:

class TestPayments(unittest.TestCase):
    ...

    def test_command_line_application():
        main = Main()
        repository = application.provide(PaymentsModule.repository)
        main.run(...)
        self.assertEqual(repository.get_payment(0), Payment(...))

This example is rather basic but it should help get you started.

Metadata

Release files for seamful 0.2.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for seamful 0.2.0
File Size Uploaded
seamful-0.2.0.tar.gz 46.1 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for seamful 0.2.0
File Interpreter ABI Platform
seamful-0.2.0-py3-none-any.whl Python 3 none any Details

Total release size: 143.8 kB

Release files / seamful-0.2.0.tar.gz

Download URL seamful-0.2.0.tar.gz
Size 46.1 kB
Tags Source
SHA-256 checksum
How to use checksums
fd8f4e2ebd933d453ec315eb182cb693c8ccfc3076410395b37c39db2d64f8da
BLAKE2b-256 checksum
How to use checksums
0ab599a9430fe57b7e983ef745d516ce98f4e6c5c314938608b30d4e6a2ec67f
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via poetry/1.5.1 CPython/3.10.6 Linux/5.19.0-46-generic

Release files / seamful-0.2.0-py3-none-any.whl

Download URL seamful-0.2.0-py3-none-any.whl
Size 97.7 kB
Tags Python 3
SHA-256 checksum
How to use checksums
3f920e099581e49ff4d0510a226b7f5361d31c536a845af25da9e482c92f08bf
BLAKE2b-256 checksum
How to use checksums
3b98ebe418df4af065b0095a37e86e27e5403194f692755a9f1f55cba57ec925
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via poetry/1.5.1 CPython/3.10.6 Linux/5.19.0-46-generic

Release history Release notifications | RSS feed

This release

0.2.0 This release

2 release files

0.1.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page