Jeremy Satterfield
Coding, Brewing and Tulsa Life

Finding Un-mocked HTTP requests in Python tests with Nose

I'm a big fan of proper unit testing with mocking. So I'm pretty disappointed when we run across a unit that is not only not mocked properly, but results in real-world consequences outside the testing environment. One case we've run into couple of times now is tests that are making actual outbound HTTP requests to remote servers. Since clients don't necessarily like getting fake information posted to their servers every time you run tests, I figured this was a good time to get to the bottom of this.

I caught this particular case while researching another broken test and noticed that one particular test was hanging for several second before passing every time. After digging in, I found that the view being tested called a method, which called a method, which executed a task that included a requests.post() call. It was quick to mock out the first method call that spawned it all, but I was worried about next time we test against that view and forget the mock again.

After a little research I found that Nose provides the ability to run package and module level setup and teardown functions. Simply add setUpPackage, setUpModule, tearDownPackage and tearDownModule functions to the __init__.py of the package or module appropriately and you're free to do any setup and cleanup you need for the package or module as a whole. This can include setting up fixtures, test databases, or, as I discovered, mocking. Placing the following code in the __init__.py of my project, quickly identified another instance where we were making outbound requests that we weren't catching.

pkg_mox = None


def setUpPackage():
    global pkg_mox
    import urllib
    import urllib2
    import mox
    import requests
    pkg_mox = mox.Mox()
    pkg_mox.StubOutWithMock(requests.api.sessions.Session, 'send')
    pkg_mox.StubOutWithMock(urllib.URLopener, 'open')
    pkg_mox.StubOutWithMock(urllib2.OpenerDirector, 'open')
    pkg_mox.ReplayAll()


def tearDownPackage():
    pkg_mox.VerifyAll()
    pkg_mox.UnsetStubs()

Cases where requests were being made, but not properly mocked, raised the "Unexpected method called" exception you'd expect, including the traceback to the offending test.

Through the years, we've gone through a few iterations on how we like making our http calls. While we've settled on the requests library for now you're always going to be dealing with legacy code. This flow catches calls to all three major request libraries, just in case. Mocking the underlying methods of the libraries catches all of the common paths for executing requests for each, while still allowing those common cases to be properly mocked in your test code.

I'm certain there are many other libraries that this could be helpful for. Now let's get to writing better unit tests.

(Comments)

Comments