Determine DAST Dependency Injection Strategy
Summary
Programs that are written in static languages like Java often rely heavily on Dependency Injection, and programs that are written in dynamic languages like Ruby often decry the need for Dependency Injection. Each of these solutions are valid within their context.
This issue proposes two alternatives for managing dependencies in DAST Python. Both choices are considered acceptable choices that would not limit the quality of product created by the DAST team. A choice should be made so that expectations are clear and the team can continue to write code in a clear and consistent manner.
Both proposals below use a SystemClock
, a ReportFormatter
and a test as an example of what each might look like. Where possible, comments on each relates to the example.
Proposal 1: Create Dependencies within classes, use DI as needed
from src.services.system_clock import SystemClock
class ReportFormatter:
def format(self):
return { 'end_time': SystemClock().now() }
from unittest.mock import MagicMock, patch
from src.report_formatter import ReportFormatter
class ReportFormatterTest(TestCase):
@patch('builtins.open')
def test_end_time(self):
with patch('SystemClock') as SystemClock:
system_clock = SystemClock.return_value
system_clock.now = MagicMock(return_value='2020-01-28T03:26:02')
report = ReportFormatter().format()
self.assertEqual(report['end_time'], '2020-01-28T03:26:02')
Notes
- It's quick and easy. Using DI may be overkill in a language like Python where constructors can be substituted in a test.
- Fewer constructors where all they do is assign variables.
- We will still need to find a DI solution where:
- We only want one instance of the class (e.g.
Configuration
). Singleton patterns may be an alternative. - Services compose other services (e.g. DAST
report_formatters
).
- We only want one instance of the class (e.g.
- If we stub/mock more than one service with
@patch
, there will be more than one level of indentation in the test method- Cam: It can support up to two levels, by using a
,
in thewith
:def test_should_should_retry_for_free_port_when_port_is_taken(self): with patch('src.system.socket') as socket, patch('src.system.random') as random: ...
- Cam: It can support up to two levels, by using a
- I don't know how to inject something other than a MagicMock. For example, how can I substitute a
FrozenClock
instance instead ofSystemClock
mock? I believeFrozenClock
would have to accept the same constructor arguments asSystemClock
.- Avielle: it'd look like this:
from unittest.mock import MagicMock, patch from src.report_formatter import ReportFormatter class FrozenClock: def now(): return '1980-10-31T00:00:00' class ReportFormatterTest(TestCase): @patch('builtins.open') def test_end_time(self): with patch('SystemClock', return_value=FrozenClock()): report = ReportFormatter().format() self.assertEqual(report['end_time'], '1980-10-31T00:00:00')
- Avielle: it'd look like this:
Proposal 2: Use Dependency Injection (without a framework)
# dependencies.py
from src.services.system_clock import SystemClock
from src.report_formatter import ReportFormatter
system_clock = SystemClock()
report_formatter = ReportFormatter(system_clock)
class ReportFormatter:
def __init__(self, system_clock):
self.system_clock = system_clock
def format(self):
return {'end_time': self.system_clock.now()}
from unittest import TestCase
from unittest.mock import MagicMock
from src.report_formatter import ReportFormatter
class ReportFormatterTest(TestCase):
def test_end_time(self):
system_clock = MagicMock(now=MagicMock(return_value="2020-01-28T03:26:02"))
report = ReportFormatter(system_clock).format()
self.assertEqual(report['end_time'], '2020-01-28T03:26:02')
Notes
-
ReportFormatter
does not need to know how to create aSystemClock
. Reduced noise in the class can lead to clearer understanding. -
ReportFormatter
is not tightly coupled toSystemClock
dependencies.- e.g. If
Locale
was passed to theSystemClock
constructor, theReportFormatter
may also requireLocale
in its constructor. - The same applies to the class that creates
ReportFormatter
. Will that class requireLocale
in its constructor? Who creates it?
- e.g. If
-
ReportFormatterTest
is not tightly coupled toSystemClock
dependencies. - The
ReportFormatter
andReportFormatterTest
aren't tightly coupled to the namespace ofSystemClock
. - All service dependencies are created in one long, ugly transaction script. Great for seeing how things are wired, horrible to look at.
- The transaction script is only able to be tested using integration tests (also: it should have a little behaviour as possible).
- Decoupled constructors mean that it is easy to create services that compose other services.
- Engineers get a build time error when creating services with cyclic dependencies.