views:

87

answers:

2

I want to achieve maximum testability in my google-app-engine app which I'm writing in python.

Basically what I'm doing is creating an all-purpose base handler which inherits the google.appengine.ext.webapp.RequestHandler. My base handler will expose common functionality in my app such as repository functions, a session object and the like.

When the WSGIApplication receives a request it will find the handler class that has been registered for the requested url, and call it's constructor and after that it will call a method called initialize passing in the request and response objects. Now, for the sake of testability I want to be able to "mock" these objects (along with my own objects). So my question is how do I go about injecting these mocks? I can override the initialize method in my base handler and check for some global "test flag" and initialize some dummy request and response objects. But it seems wrong (in my mind at least). And how do I go about initializing my other objects (which may depend on the request and response objects)?

As you can probably tell I'm a little of a python newbie so any recommendations would be most welcome.

EDIT:

It has been pointed out to me that this question was a little hard to answer without some code, so here goes:

from google.appengine.ext import webapp
from ..utils import gmemsess
from .. import errors
_user_id_name = 'userid'

class Handler(webapp.RequestHandler):
    '''
    classdocs
    '''

    def __init__(self):
        '''
        Constructor
        '''
        self.charset = 'utf8'
        self._session = None

    def _getsession(self):
        if not self._session:
            self._session = gmemsess.Session(self)
        return self._session

    def _get_is_logged_in(self):
        return self.session.has_key(_user_id_name)

    def _get_user_id(self):
        if not self.is_logged_in:
            raise errors.UserNotLoggedInError()
        return self.session[_user_id_name]

    session = property(_getsession)
    is_logged_in = property(_get_is_logged_in)
    user_id = property(_get_user_id)

As you can see, no dependency injection is going on here at all. The session object is created by calling gmemsess.Session(self). The Session class expects a class which has a request object on it (it uses this to read a cookie value). In this case, self does have such a property since it inherits from webapp.RequestHandler. It also only has the object on it because after calling (the empty) constructor, WSGIApplication calls a method called initialize which sets this object (and the response object). The initialize method is declared on the base class (webapp.RequestHandler). It looks like this:

def initialize(self, request, response):
    """Initializes this request handler with the given Request and 
     Response."""
    self.request = request
    self.response = response

When a request is made, the WSGIApplication class does the following:

def __call__(self, environ, start_response):
    """Called by WSGI when a request comes in."""
    request = self.REQUEST_CLASS(environ)
    response = self.RESPONSE_CLASS()
    WSGIApplication.active_instance = self
    handler = None
    groups = ()
    for regexp, handler_class in self._url_mapping:
        match = regexp.match(request.path)
        if match:
            handler = handler_class()
            handler.initialize(request, response)
            groups = match.groups()
            break

    self.current_request_args = groups
    if handler:
        try:
            method = environ['REQUEST_METHOD']
            if method == 'GET':
                handler.get(*groups)
            elif method == 'POST':
                handler.post(*groups)
            '''SNIP'''

The lines of interest are those that say:

     handler = handler_class()
     handler.initialize(request, response)

As you can see, it calls the empty constructor on my handler class. And this is a problem for me, because what I think I would like to do is to inject, at runtime, the type of my session object, such that my class would look like this instead (fragment showed):

    def __init__(self, session_type):
        '''
        Constructor
        '''
        self.charset = 'utf8'
        self._session = None
        self._session_type = session_type

    def _getsession(self):
        if not self._session:
            self._session = self._session_type(self)
        return self._session

However I can't get my head around how I would achieve this, since the WSGIApplication only calls the empty constructor. I guess I could register the session_type in some global variable, but that does not really follow the philosophy of dependency injection (as I understand it), but as stated I'm new to python, so maybe I'm just thinking about it the wrong way. In any event I would rather pass in a session object instead of it's type, but this looks kinda impossible here.

Any input is appreciated.

Thanks.

+2  A: 

The simplest way to achieve what you want would be to create a module-level variable containing the class of the session to create:

# myhandler.py
session_class = gmemsess.Session

class Handler(webapp.Request
    def _getsession(self):
        if not self._session:
            self._session = session_class(self)
        return self._session

then, wherever it is that you decide between testing and running:

import myhandler

if testing:
    myhandler.session_class = MyTestingSession

This leaves your handler class nearly untouched, leaves the WSGIApplication completely untouched, and gives you the flexibility to do your testing as you want.

Ned Batchelder
Ok, but then how do you suggest I "mock" the request and response objects? Something along the lines `if testing: myhandler_instance.request = TestRequest()` ?
klausbyskov
request and response are passed into initialize, which you can override.
Ned Batchelder
+1  A: 

Why not just test your handlers in isolation? That is, create your mock Request and Response objects, instantiate the handler you want to test, and call handler.initialize(request, response) with your mocks. There's no need for dependency injection here.

Nick Johnson