pragmatic pytesting
Daniel J. Rocco, Ph.D.
def pytest():
"""A mature, full-featured python test runner and tool-suite
* easy to write tests, easy to run, easy to maintain
* supplied tools address most common testing needs
* orthogonal architecture provides advanced features
with minimal intrusion"""
pytest.main()
def test_simple_pass(): """The simplest passing test""" pass
def test_simple_assertions(): """Demonstrates passing tests that use assert""" assert True assert [1] assert dict(pytest='awesome')
def test_negative_assertions(): """Demonstrates passing tests that use negated assertions""" assert not False assert not [] assert not dict()
def test_simple_fail(): assert False def test_simple_fail_with_message(): assert False, 'Snap, something went wrong...'
def test_uncaught_exceptions_fail_test(): open('this is not the file you are looking for...')
def test_expected_exception(): """Demonstrates pytest's raises context manager""" with pytest.raises(ZeroDivisionError): 1/0 with pytest.raises(IOError): open('/some/bogus/file.txt')
Installation:
$ virtualenv my_project $ cd my_project ; . bin/activate $ pip install pytest pytest-cov mock $ mkdir my_package ; mkdir testsFire it up:
$ py.test tests/![]()
def get_random_number(): """Algorithm courtesy of http://xkcd.com/221/""" return 4 def test_get_random_number(): assert 4 == get_random_number()
def will_it_blend(thing): """Will the thing blend? >>> will_it_blend('a car') True >>> will_it_blend('tomato juice') False """ return thing == 'a car'Invoke pytest with the
--doctest-modules
switch
package/ __init__.py module.py ... test_package.py test_package_module.py ...
proj/ package/ __init__.py foo_bar_baz.py ... package2/ ... tests/ test_foo_bar_baz.py setup.py
This layout simplifies coverage testing
Wormly clone; basic requirements:
- periodically ping a URL
- check for valid response code, presence or absence of certain text
- timeout | bad response | bad text → failure
- send notification on failure
- track response history
class Response(object): """Abstraction around urlopen's various response types""" ... class Monitor(object): def __init__(self, url): self.url = url def ping(self): try: url_response = urlopen(self.url) response = Response(response=url_response) except IOError, e: response = Response(exception=e) return response
def test_valid_local_http_response_should_yield_positive(): monitor = Monitor('http://localhost:8000') response = monitor.ping() assert response assert httplib.OK == response.response_codeNB: for this to work, you need a running web server:
$ python -m SimpleHTTPServerwhich lets us know that we have
Given a mechanism for opening URLs,
When I check the availability of a given URL,
- good response (e.g. 200) should yield success response
- timeout should yield failure response
- bad response code (e.g. 404) should yield failure response
- good response with bad text in the response should yield failure response
behavior on: successful response, failed response, successful response with good/bad text, timeout
meta-answer: need to think carefully about what you are testing and why
DI pattern core idea: function's dependencies should appear in its signature
def dependencies_go(here=True): dependencies = not here
core idea: pass function's dependencies to it on call
rationale:
- communication: function communicates its dependencies in its
- signature, rather than having implicit dependencies scattered throughout its implementation
- isolation: DI fn is loosely coupled to the rest of the system: deps
- flow to it from caller
- testability: much easier to provide alternative test implementations
- of deps
class Monitor(object): def __init__(self, url): self.url = url def ping(self): try: url_response = urlopen(self.url) ^^^^^^^ ...
Monitor has a hard dependency on urlopen that makes it difficult to test.
- What happens if the network is down?
- How can I easily test error codes like 401 & 403?
- What if I need non-default behavior, e.g. NTLM auth?
class Monitor(object): open = staticmethod(urlopen) def __init__(self, url, opener_director=None): self.url = url if opener_director: self.open = opener.open def ping(self): try: url_response = self.open(self.url) ...
Using dependency injection allows us to break the hard dependency on urlopen, although for convenience it is still the default.
Advantages:
- By default, works exactly as it used to
- Monitor is now more flexible: I can use any implementation that conforms to OpenerDirector's interface
- For testing, I can pass mock objects that provide responses mimicking real scenarios without actually talking over the network
# Basic HTTP Auth
auth_handler = urllib2.HTTPBasicAuthHandler()
auth_handler.add_password(...)
opener = urllib2.build_opener(auth_handler)
Monitor('http://super.secret.com', opener)
# Custom user agent
opener = urllib2.build_opener()
opener.addheaders = [('User-agent', 'Mozilla/5.0')]
Monitor('http://abolish-all-robots.org', opener)
Power tool: Michael Foord's Mock library
Mock instances are callable:
>>> from mock import Mock >>> mock_fn = Mock(return_value=42) >>> mock_fn() 42
They provide useful information to your tests:
>>> mock_fn.assert_called_once_with()
By default, accessing an attribute on a Mock yields a new Mock, making object mocking trivial:
>>> mock_obj = Mock() >>> isinstance(mock_obj.foo, Mock) True >>> mock_obj.foo.return_value = 'I\'m a return value!' >>> mock_obj.foo('I\'m an argument!') "I'm a return value!" >>> mock_obj.foo.assert_called_once_with('I\'m an argument!')
OpenerDirector's open
method → response object
response.code
→ HTTP status
def mock_opener_director(response_code=httplib.OK): """Build a mock OpenerDirector instance.""" mock_response = Mock(code=response_code) open = Mock(return_value=mock_response) opener_director = Mock(open=open) return opener_director
def test_valid_local_http_response_should_yield_positive(): opener_director = mock_opener_director() url = 'http://localhost:8000' monitor = Monitor(url, opener_director=opener_director) response = monitor.ping() opener_director.open.assert_called_once_with(url) assert response assert httplib.OK == response.response_code
Or, rather, failing to fail...
def test_not_found_should_yield_negative(): opener_director = mock_opener_director( response_code=httplib.NOT_FOUND ) monitor = Monitor('http://localhost:8000/404.html', opener_director=opener_director) response = monitor.ping() > assert not response E assert not <pyping.model.Response object at 0x...>
def mock_opener_director(response_code=httplib.OK): mock_response = Mock(code=response_code) open = Mock(return_value=mock_response) ...
![]()
Wait, that isn't right!
What about a
response_code
that's an error?
def mock_opener_director(response_code=httplib.OK): mock_response = Mock(code=response_code) def _side_effect(*args, **kw): if response_code < 300: return DEFAULT else: error = IOError() error.code = response_code raise error open = Mock(return_value=mock_response, side_effect=_side_effect) opener_director = Mock(open=open) return opener_director
stop after the first failure:
$ py.test -x ...fire up ye olde debugger on failure:
$ py.test --pdb ...
suppress output capture:
$ py.test -s ...gotcha covered:
$ py.test --cov pyping \ --cov-report=html \ tests/
- the
- inevitable
- question
![]()