一个模拟/存根python模块如何能像urllib

我需要testing一个函数,需要使用urllib.urlopen(它也使用urllib.urlencode)在外部服务器上查询页面。 服务器可能closures,页面可能改变; 我不能依靠它进行testing。

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

另一个简单的方法是让你的testing覆盖urllib的urlopen()函数。 例如,如果你的模块有

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

你可以像这样定义你的testing:

 import mymodule def dummy_urlopen(url): ... mymodule.urllib.urlopen = dummy_urlopen 

然后,当你的testing调用dummy_urlopen()函数时, dummy_urlopen()将被调用,而不是真正的urlopen() 。 像Python这样的dynamic语言使得对方法和类进行testing非常容易。

有关testing依存关系的更多信息,请参阅http://softwarecorner.wordpress.com/上的我的博客文章。;

我正在使用模拟的修补程序装饰器:

 from mock import patch [...] @patch('urllib.urlopen') def test_foo(self, urlopen_mock): urlopen_mock.return_value = MyUrlOpenMock() 

你给Mox一下吗? 它应该做你需要的一切。 以下是一个简单的互动会话,说明您需要的解决scheme:

 >>> 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 ...> 

HTTPretty的工作方式与FakeWeb完全相同。 HTTPretty在套接字层中工作,所以它应该拦截任何python http客户端库。 这是testing对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" 

处理这个问题的最好办法可能是分解代码,这样处理页面内容的逻辑就会从提取页面的代码中分离出来。

然后将fetcher代码的一个实例传递给处理逻辑,然后你可以很容易地用一个模拟fetcherreplace它来进行unit testing。

例如

 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 

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

 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' 

最简单的方法是改变你的函数,以便它不一定使用urllib.urlopen。 假设这是你的原始function:

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

添加一个参数,它是用来打开URL的函数。 那么你可以提供一个模拟function来做你需要的任何事情:

 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)