[pytest-dev] Working with other dependency injection systems

Kai Groner kai at gronr.com
Tue Sep 8 20:33:32 CEST 2015


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?


Kai

best,
> holger
>
>
> > > > The second problem I'm having is the py.test fixture system is
> trying to
> > > > resolve arguments that are provided by this other injector.  How can
> I
> > > tell
> > > > it that some of these don't need to be provided?  My current
> solution is
> > > to
> > > > blind py.test with a wrapper function with a *a, **kw signature.  I
> use
> > > > functools.wraps, so annotations are introspectable for the other
> > > injector,
> > > > and delete the __wrapped__ attribute to prevent py.test from
> > > introspecting
> > > > it.  Is there a nicer, and perhaps less blunt, way to influence the
> > > funcarg
> > > > fixture behaviors?  I've tried a couple things with the
> > > > pytest_collection_modifyitems hook, but haven't gotten anything that
> > > works
> > > > yet.
> > > >
> > > > Other details to know about this injection system:
> > > > - we want to build and teardown the injector with each test
> > > > - we may want to configure the injector differently for some tests
> > > > (possibly with fixture data from py.test)
> >
> >
> >
> > Kai
>
> --
> about me:    http://holgerkrekel.net/about-me/
> contracting: http://merlinux.eu
>
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://mail.python.org/pipermail/pytest-dev/attachments/20150908/9a1dc434/attachment.html>


More information about the pytest-dev mailing list