如何使用 urllib 这样的 mock/stub python 模块

我需要测试一个函数,该函数需要使用 urllib.urlopen (它还使用 urllib.urlencode)在外部服务器上查询页面。服务器可能关闭,页面可能更改; 我不能依赖它进行测试。

控制 urllib.urlopen 返回值的最佳方法是什么?

55044 次浏览

处理这个问题的最佳方法可能是分割代码,这样处理页面内容的逻辑就可以从获取页面的代码中分离出来。

然后将 fetcher 代码的一个实例传递到处理逻辑中,然后您就可以轻松地将其替换为单元测试的模拟 fetcher。

例如:。

class Processor(oject):
def __init__(self, fetcher):
self.m_fetcher = fetcher


def doProcessing(self):
## use self.m_fetcher to get page contents


class RealFetcher(object):
def fetchPage(self, url):
## get real contents


class FakeFetcher(object):
def fetchPage(self, url):
## Return whatever fake contents are required for this test

最简单的方法是更改函数,使其不必使用 urllib.urlopen。假设这是你最初的功能:

def my_grabber(arg1, arg2, arg3):
# .. do some stuff ..
url = make_url_somehow()
data = urllib.urlopen(url)
# .. do something with data ..
return answer

添加一个参数,该参数是用来打开 URL 的函数。然后,您可以提供一个 mock 函数来执行任何您需要的操作:

def my_grabber(arg1, arg2, arg3, urlopen=urllib.urlopen):
# .. do some stuff ..
url = make_url_somehow()
data = urlopen(url)
# .. do something with data ..
return answer


def test_my_grabber():
my_grabber(arg1, arg2, arg3, urlopen=my_mock_open)

另一种简单的方法是让测试覆盖 urllib 的 urlopen()函数

import urllib


def some_function_that_uses_urllib():
...
urllib.urlopen()
...

你可以这样定义你的测试:

import mymodule


def dummy_urlopen(url):
...


mymodule.urllib.urlopen = dummy_urlopen

然后,当您的测试调用 mymodule中的函数时,将调用 dummy_urlopen()而不是实际的 urlopen()。像 Python 这样的动态语言使得为了测试而删除方法和类变得非常容易。

请参阅我在 http://softwarecorner.wordpress.com/上的博客文章,了解更多关于为测试消除依赖关系的信息。

你看了 莫克斯一眼吗?它应该能满足你的一切需求。下面是一个简单的互动会议,说明了你需要的解决方案:

>>> import urllib
>>> # check that it works
>>> urllib.urlopen('http://www.google.com/')
<addinfourl at 3082723820L ...>
>>> # check what happens when it doesn't
>>> urllib.urlopen('http://hopefully.doesnotexist.com/')
#-- snip --
IOError: [Errno socket error] (-2, 'Name or service not known')


>>> # OK, let's mock it up
>>> import mox
>>> m = mox.Mox()
>>> m.StubOutWithMock(urllib, 'urlopen')
>>> # We can be verbose if we want to :)
>>> urllib.urlopen(mox.IgnoreArg()).AndRaise(
...   IOError('socket error', (-2, 'Name or service not known')))


>>> # Let's check if it works
>>> m.ReplayAll()
>>> urllib.urlopen('http://www.google.com/')
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "/usr/lib/python2.5/site-packages/mox.py", line 568, in __call__
raise expected_method._exception
IOError: [Errno socket error] (-2, 'Name or service not known')


>>> # yay! now unset everything
>>> m.UnsetStubs()
>>> m.VerifyAll()
>>> # and check that it still works
>>> urllib.urlopen('http://www.google.com/')
<addinfourl at 3076773548L ...>

我正在使用 模仿秀补丁修饰器:

from mock import patch


[...]


@patch('urllib.urlopen')
def test_foo(self, urlopen_mock):
urlopen_mock.return_value = MyUrlOpenMock()

如果你甚至不想加载这个模块:

import sys,types
class MockCallable():
""" Mocks a function, can be enquired on how many calls it received """
def __init__(self, result):
self.result  = result
self._calls  = []


def __call__(self, *arguments):
"""Mock callable"""
self._calls.append(arguments)
return self.result


def called(self):
"""docstring for called"""
return self._calls


class StubModule(types.ModuleType, object):
""" Uses a stub instead of loading libraries """


def __init__(self, moduleName):
self.__name__ = moduleName
sys.modules[moduleName] = self


def __repr__(self):
name  = self.__name__
mocks = ', '.join(set(dir(self)) - set(['__name__']))
return "<StubModule: %(name)s; mocks: %(mocks)s>" % locals()


class StubObject(object):
pass

然后:

>>> urllib = StubModule("urllib")
>>> import urllib # won't actually load urllib


>>> urls.urlopen = MockCallable(StubObject())


>>> example = urllib.urlopen('http://example.com')
>>> example.read = MockCallable('foo')


>>> print(example.read())
'foo'

HTTpretty 的工作方式与 FakeWeb 完全相同。HTTpretty 在套接字层中工作,因此它应该可以拦截任何 pythonhttp 客户端库。这是针对 urllib2、 httplib2和请求进行的战斗测试

import urllib2
from httpretty import HTTPretty, httprettified




@httprettified
def test_one():
HTTPretty.register_uri(HTTPretty.GET, "http://yipit.com/",
body="Find the best daily deals")


fd = urllib2.urlopen('http://yipit.com')
got = fd.read()
fd.close()


assert got == "Find the best daily deals"

添加到 Clint Miller 的答案中,为了做到这一点,我必须创建一个假的类,它实现了一个像下面这样的 read 方法:

class FakeURL:
def read(foo):
return '{"some":"json_text"}'

然后将 urllib2.open 删除:

# Stub out urllib2.open.
def dummy_urlopen(foo, bar, baz):
return FakeURL()
urllib2.urlopen = dummy_urlopen