Closed snewell92 closed 11 months ago
Hi, thanks for the nice words.
I do agree that at the moment testing autowire targets can be a bit difficult. The idea of temporarily overriding dependencies with custom implementations also sounds good.
When autowiring, the container always refers to __initialized_objects
before doing anything else, to see if an instance already exists. We can take advantage of this to trick it into using the mock.
I hope this helps resolving your issue until the feature is (hopefully soon) supported!
Example:
# Set up: our service and a view
@container.register
class GreeterService:
def greet(self, name) -> str:
return f"Hi {name}"
@app.get("/greet")
@container.autowire
def home(greeter: GreeterService) -> str:
name = request.args.get("name")
return greeter.greet(name)
# In your tests
class GreetViewTest(unittest.TestCase):
def setUp(self) -> None:
self.client = create_app().test_client()
def test_mock_autowired(self):
res = self.client.get("/greet?name=wireup")
self.assertEqual("Hi wireup", res.text)
# Create a mock for the "greet" method.
mock = MagicMock()
mock.greet = MagicMock(return_value="It's mocked!")
# Set the initialized object to the one you want to override
# The key is a tuple consisting of the class and the qualifier.
# which is by default to None unless you set something using
# qualifier="..." during registration.
container._DependencyContainer__initialized_objects[(GreeterService, None)] = mock
res = self.client.get("/greet?name=wireup")
self.assertEqual("It's mocked!", res.text)
We can also create a context manager for this. This is something hacky I threw together you can use to unblock yourself:
class ContainerOverride:
def __init__(
self,
dependency_container: DependencyContainer,
override: type,
new_value: Any,
qualifier: ContainerProxyQualifierValue = None,
):
self.container = dependency_container
self.override = override
self.new_value = new_value
self.qualifier = qualifier
self.existing_instance = dependency_container._DependencyContainer__initialized_objects.get((override, qualifier)) # noqa
def set_value(self, value: Any) -> None:
self.container._DependencyContainer__initialized_objects[(self.override, self.qualifier)] = value # noqa
def __enter__(self):
self.set_value(self.new_value)
def __exit__(self, exc_type, exc_val, exc_tb):
if self.existing_instance:
self.set_value(self.existing_instance)
else:
del self.container._DependencyContainer__initialized_objects[(self.override, self.qualifier)]
return True
Which we can use as follows
with ContainerOverride(container, override=GreeterService, new_value=greeter_mock):
res = self.client.get("/greet?name=wireup")
self.assertEqual("It's mocked!", res.text)
Adjust as necessary to be able to mock multiple things simoultaneously.
This worked perfectly. 💯 Thanks so much! After running with the above hacky solution for a wee while, I'm happy to contribute this back if we tinker with it. I'll put it in a gist so versioning can be seen.
This Gist will be where I put any changes to ContainerOverride
that I'm able to get working in my project as I tinker with it (we use black/flake8/mypy extensively so it'll be 'squeaky clean' in that regard #soon and I can look at extending it where necessary)
Hiya! Greatly enjoying this lib; we are exploring DI in one of our services and I've got a PR to add wireup and just use the singleton; the only issue the team has run into is we're not able to inject a mock into the flask route handler. Normally, like with services, we just directly use the target object and instantiate it in the unit test with mocks, however this won't work for flask endpoints. Consider the following code
And this unit test with flask's test app mechanism
I've tried setting up multiple containers to have an empty test one and a normal prod one but it has proven... quite difficult to use.
Could we perhaps have a context manager style solution?