[pytest-dev] Working with other dependency injection systems

holger krekel holger at merlinux.eu
Thu Sep 10 09:12:50 CEST 2015


On Tue, Sep 08, 2015 at 14:33 -0400, Kai Groner wrote:
> On Mon, Sep 7, 2015 at 4:01 AM, holger krekel <holger at merlinux.eu> wrote:
> 
> > On Thu, Aug 13, 2015 at 17:47 -0400, Kai Groner wrote:
> > > Hi holger,
> > >
> > > Thanks for your response.  Sorry I haven't followed up sooner.
> >
> > same here :)
> >
> > > On Thu, May 7, 2015 at 7:21 AM, holger krekel <holger at merlinux.eu>
> > wrote:
> > >
> > > > Hi Kai,
> > > >
> > > > On Thu, Apr 30, 2015 at 19:28 -0400, Kai Groner wrote:
> > > > > I'm trying to figure out how I can test a code base that uses an
> > existing
> > > > > dependency injection system.  I've run into two problems, and I have
> > > > > solutions for each of them but I think maybe there is a better way,
> > so
> > > > I'm
> > > > > looking for some advice.
> > > >
> > > > Could you provide a simple abstract example?
> > > > Here and also in the following i don't really understand the
> > background.
> > > >
> > >
> > > The DI system we are using is called jeni.  I'll try to keep things
> > simple
> > > here so you don't need to know a lot about it, but here's the url.
> > > https://github.com/rduplain/jeni-python
> > >
> > > This is untested code, that I hope will be illustrative.  If you think it
> > > would be helpful to run it, let me know and I will make sure it works.
> > >
> > > We write code sort of like this:
> > >
> > > @route('login', method='POST')
> > > @jeni.annotate
> > > def login(
> > >         username: 'form:username',
> > >         password: 'form:password',
> > >         user_lookup: 'user_lookup',
> > >         session_init: 'session_init'):
> > >     user = user_lookup(username)
> > >     if user is None:
> > >         raise ValueError
> > >     if not user.check_password(password):
> > >         raise ValueError
> > >     session_init(user)
> > >
> > >
> > > Here is an example of an injector prototyped with a trivial user_lookup
> > > implementation:
> > >
> > > class Injector(jeni.Injector): pass
> > >
> > > @Injector.factory
> > > def user_lookup_factory():
> > >     class User:
> > >         def __init__(self, username, password=None):
> > >             self.username, self.password = username, password
> > >
> > >         def check_password(self, password):
> > >             return password == self.password
> > >
> > >     def user_lookup(username):
> > >         return User(username, username)
> > >
> > >
> > > Calling this, looks like:
> > >
> > >
> > > with Injector() as inj:
> > >     inj.apply(login)
> > >
> > > There is a partial application mechanism, that creates a wrapper that
> > > resolves injector bindings at call time.
> > >
> > > @jeni.annotate
> > > def test_login(login: jeni.partial(login)):
> > >
> > >     login(username='kai', password='kai')
> > >
> > >     with raises(LookupError):
> > >
> > >         login(username='kai', password='KAI')
> > >
> > >
> > > with Injector() as inj:
> > >
> > >     inj.apply(test_login)
> > >
> > > Because we need to write a lot of tests, I want to avoid writing the with
> > > block with every test.  I thought maybe I could use py.test hooks to do
> > > this.
> >
> > Please checkout
> > https://pytest.org/latest/plugins.html#hookwrapper-executing-around-other-hooks
> >
> > It should be the right hook to systematically do your with-injector
> > logic.
> 
> 
> I think so.  I just need to get it to run the test without certain
> "fixtures".
> 
> > > The first problem is how do I override how a test using this injector is
> > > > > called?  I think I want to use the pytest_runtest_call hook, but it
> > still
> > > > > tries to invoke the test without the injector.  My current solution
> > is to
> > > > > use the experimental hookwrapper mechanism to replace item.obj with a
> > > > > partially bound version, then restore it afterward.
> > > >
> > >
> > > Here my problem is that py.test looks for a ``login`` fixture and when it
> > > finds none it decides the test cannot proceed.
> >
> > You probably need to provide this "login" fixture even if it's empty
> > and work is done in the hook above. not sure i get all your semantics
> > 100 % though.
> >
> 
> The thing is that these aren't really fixtures and the dependencies are
> selected by annotation rather than argument name.  What login means in one
> test won't match what login means in another test.  Even when it is the
> same service, it will be a new instance/partial created by a new injector
> with new services.
> 
> I do want to use py.test fixtures for parametrization.  Is there a way I
> can tell py.test that certain arguments aren't fixtures and it shouldn't
> worry about providing them?

Not really.  Why can't you have a fixture function that selects an 
implementation based on annotations associated with the test function?

holger


More information about the pytest-dev mailing list