像urllib这样的模拟/存根python模块如何


72

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

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


不太一样,但是我想大多数人都使用requests如何模拟请求和响应?
马丁·托马

Answers:


98

另一个简单的方法是让您的测试覆盖urllib的urlopen()功能。例如,如果您的模块具有

import urllib

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

您可以这样定义测试:

import mymodule

def dummy_urlopen(url):
    ...

mymodule.urllib.urlopen = dummy_urlopen

然后,当您的测试调用中的函数时mymoduledummy_urlopen()将调用而不是real urlopen()。像Python这样的动态语言,使测试方法和类的存根非常容易。

请参阅我的博客文章,网址http://softwarecorner.wordpress.com/,以获取有关测试依赖项的更多信息。


11
进行测试的猴子补丁很方便。确实,这可能是典型的“好猴子补丁”示例。
S.Lott

visionandexecution.org似乎已关闭。还有另一个链接,还是现在消失了?
Mu Mind

1
我已经很长时间没有发布到博客了,但是我确实将其移植到softwarecorner.wordpress.com
Clint Miller

14
谨防!如果您未将模拟对象显式重置回原始值,则将对测试模块中的urlopen的所有实例以及模块中的其他类进行模拟。当然,在这种情况下,我不确定为什么有人会在单元测试中进行网络调用。我建议使用类似“ with patch ...”或@patch()的方法,它们可以使您对要模拟的内容以及达到最大限制的方式进行更明确的控制。
Keshi

71

我正在使用Mock的补丁装饰器:

from mock import patch

[...]

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

5
太糟糕了,它在修补模块功能时不起作用:/(至少不是0.7.2)
Tommaso Barbugli 2012年

3
不是100%正确,如果您在修补程序之前导入该函数,它将起作用,否则,修补程序会以静默方式失败(没有错误,只是没有任何修补程序:/)
Tommaso Barbugli 2012年

2
好地方 修补程序在找不到相关模块时会抛出错误,而不仅仅是静默失败。
fatuhoku 2013年

2
这给了我一个错误。找不到固定装置'urlopen_mock'
Pratik Khadloya 2014年

26

你给莫克斯看了吗?它应该做您需要的一切。这是一个简单的交互式会话,说明您需要的解决方案:

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

引用pypi.org/project/mox:“不鼓励使用此库。鼓励人们使用pypi.python.org/pypi/mock代替与Python 3中提供的unittest.mock库匹配的库。”
弗洛里安

14

HTTPretty的工作方式与FakeWeb完全相同。HTTPretty在套接字层工作,因此它应能拦截任何python http客户端库。经过针对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"

在2013年,这绝对是最好的答案。伙计们,我们来投票一下Falcão的超赞图书馆!
fatuhoku 2013年

从Obj-C角度来看,我正在寻找类似OHHTTPStubs的Python。我很高兴找到HTTPretty。
fatuhoku 2013年

9

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

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 import * ...的调用者将无法获得所需的功能
Erik Aronesty

8

处理此问题的最佳方法可能是拆分代码,以便将处理页面内容的逻辑与获取页面的代码分开。

然后将获取程序代码的实例传递到处理逻辑中,然后可以轻松地用模拟获取程序替换它以进行单元测试。

例如

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

3

最简单的方法是更改​​函数,使其不必使用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的函数。然后,您可以提供一个模拟函数来执行所需的任何操作:

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)

3
不确定我是否喜欢让被测灯具知道配置详细信息...但是,这确实可行。
S.Lott

1
我看不到参数化函数有什么问题。这里不知道urlopen可能如何被伪造或为什么被伪造,只是它可能会发生。
Ned Batchelder

0

要添加到克林特·米勒的答案上,要做到这一点,我必须创建一个伪类,该类实现如下所示的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
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.