Package: seamful
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 usingworkingtest 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:fromseamfulimportModule,ResourceclassPaymentsModule(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:classPaymentsProvider(Provider,module=PaymentsModule):defget_repository(self)->PaymentsRepository:returnPaymentsRepository(...)defget_service(self,repository:PaymentsModule.repository)->PaymentsService:returnPaymentsService(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 parameterif 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 Providerit must state which module it provides for via the module= parameterit 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:fromseamfulimportApplicationapplication=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.applicationimportapplicationclassMain:def__init__(self):self.service=application.provide(PaymentsModule.service)defrun(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 thePaymentsRepositoryuses a database that we wish not to use in our integration tests. We can create a test_double:classInMemoryPaymentsRepository(PaymentsRepository):def__init__(self):self.payments=[]defsave_payment(self,payment:Payment)->int:self.payments.append(payment)returnlen(self.payments)-1defget_payment(self,id:int)->Payment:returnself.payments[id]deflist_payments(self)->Iterable[Payment]:returnself._payments[:]And then we can configure the application to provide that implementation instead of the real one:importunittestfrom.applicationimportapplicationclassPaymentProviderForTests(PaymentsProvider):defget_repository(self)->PaymentsRepository:returnInMemoryPaymentsRepository()classTestPayments(unittest.TestCase):defsetUp(self):application.tamper()application.install_provider(PaymentProviderForTests)application.ready()deftearDown():application.restore()deftest_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:classTestPayments(unittest.TestCase):...deftest_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.