Je dois tester une fonction qui doit interroger une page sur un serveur externe en utilisant urllib.urlopen (il utilise également urllib.urlencode). Le serveur pourrait être en panne, la page pourrait changer; Je ne peux pas compter dessus pour un test.
Quel est le meilleur moyen de contrôler les retours d'urllib.urlopen?
Une autre approche simple consiste à remplacer votre test par la fonction urlopen()
de urllib. Par exemple, si votre module a
import urllib
def some_function_that_uses_urllib():
...
urllib.urlopen()
...
Vous pouvez définir votre test comme ceci:
import mymodule
def dummy_urlopen(url):
...
mymodule.urllib.urlopen = dummy_urlopen
Ensuite, lorsque vos tests invoqueront des fonctions dans mymodule
, dummy_urlopen()
sera appelé à la place de la fonction urlopen()
réelle. Les langages dynamiques tels que Python facilitent l'extraction de méthodes et de classes à des fins de test.
Voir les articles de mon blog à l'adresse http://softwarecorner.wordpress.com/ pour plus d'informations sur l'élimination des dépendances lors des tests.
J'utilise Mock's patch decorator:
from mock import patch
[...]
@patch('urllib.urlopen')
def test_foo(self, urlopen_mock):
urlopen_mock.return_value = MyUrlOpenMock()
Avez-vous donné Mox un regard? Il devrait faire tout ce dont vous avez besoin. Voici une session interactive simple illustrant la solution dont vous avez besoin:
>>> 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 fonctionne exactement de la même manière que FakeWeb. HTTPretty fonctionne dans la couche socket, il devrait donc intercepter toutes les bibliothèques clientes python http. Il est testé contre urllib2, httplib2 et les requêtes
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"
Le meilleur moyen de gérer cela est probablement de scinder le code afin que la logique qui traite le contenu de la page soit séparée du code qui récupère la page.
Ensuite, passez une instance du code d'extraction dans la logique de traitement, puis remplacez-la facilement par un simulateur d'extraction pour le test unitaire.
par exemple.
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
Si vous ne voulez même pas charger le module:
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.__= 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
Et alors:
>>> 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'
Le moyen le plus simple est de changer votre fonction afin qu'elle n'utilise pas nécessairement urllib.urlopen. Disons que ceci est votre fonction d'origine:
def my_grabber(arg1, arg2, arg3):
# .. do some stuff ..
url = make_url_somehow()
data = urllib.urlopen(url)
# .. do something with data ..
return answer
Ajoutez un argument qui est la fonction à utiliser pour ouvrir l'URL. Ensuite, vous pouvez fournir une fonction fictive pour faire tout ce dont vous avez besoin:
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)
Ajoutant à la réponse de Clint Miller, pour ce faire, j'ai dû créer une fausse classe qui implémente une méthode de lecture comme celle-ci:
class FakeURL:
def read(foo):
return '{"some":"json_text"}'
Puis stub out urllib2.open:
# Stub out urllib2.open.
def dummy_urlopen(foo, bar, baz):
return FakeURL()
urllib2.urlopen = dummy_urlopen