python testing using mock and pytest
TRANSCRIPT
unittest.mock
Defn: unittest.mock is a library for testing in Python. It allows you to replace parts of your system under test with mock objects and make assertions about how they have been used.
• Using Mock you can replace/mock any dependency of your code.
• Unreliable or expensive parts of code are mocked using Mock, e.g. Networks, Intensive calculations, posting on a website, system calls, etc.
• As a developer you want your calls to be right rather than going all the way to final output.
• So to speed up your automated unit-tests you need to keep out slow code from your test runs.
>>> from unittest.mock import Mock>>> m = Mock()>>> m<Mock id='140457934010912'>>>> m.some_value = 23>>> m.some_value23>>> m.other_value<Mock name='mock.other_value' id='140457789462008'>
Mock Objects - Basics
>>> m.get_value(value=42)<Mock name='mock.get_value()' id='140457789504816'>>>> m.get_value.assert_called_once_with(value=42)>>> m.get_value.assert_called_once_with(value=2) raise AssertionError(_error_message()) from causeAssertionError: Expected call: get_value(value=2)Actual call: get_value(value=42)
• Flexible objects that can replace any part of code.
• Creates attributes when accessed.• Records how objects are being accessed.• Using this history of object access you can make
assertions about objects.
More about Mock objects
>>> from unittest.mock import Mock>>> config = {... 'company': 'Lenovo',... 'model': 'Ideapad Z510',... 'get_sticker_count.return_value': 11,... 'get_fan_speed.side_effect': ValueError... }>>> m = Mock(**config)>>> m.company'Lenovo'>>> m.get_sticker_count()11>>> m.get_fan_speed() raise effectValueError
Customize mock objects
Using spec to define attr
>>> user_info = ['first_name', 'last_name', 'email']>>> m = Mock(spec=user_info)>>> m.first_name<Mock name='mock.first_name' id='140032117032552'>>>> m.address raise AttributeError("Mock object has no attribute %r" % name)AttributeError: Mock object has no attribute 'address'
Automatically create all specs>>> from unittest.mock import create_autospec>>> import os>>> m = create_autospec(os)>>> m.Display all 325 possibilities? (y or n)m.CLD_CONTINUED m.forkptym.CLD_DUMPED m.fpathconfm.CLD_EXITED m.fsdecode[CUT]m.fchown m.walkm.fdatasync m.writem.fdopen m.writevm.fork
Using Mock through patch
• Replaces a named object with Mock object• Also can be used as decorator/context manager
that handles patching module and class level attributes within the scope of a test.
1 # main.py 2 import requests 3 import json 4 5 def upload(text): 6 try: 7 url = 'http://paste.fedoraproject.org/' 8 data = { 9 'paste_data': text, 10 'paste_lang': None, 11 'api_submit': True, 12 'mode': 'json' 13 } 14 reply = requests.post(url, data=data) 15 return reply.json() 16 except ValueError as e: 17 print("Error:", e) 18 return None 19 except requests.exceptions.ConnectionError as e: 20 print('Error:', e) 21 return None 22 except KeyboardInterrupt: 23 print("Try again!!") 24 return None 25 26 if __name__ == '__main__': 27 print(upload('Now in boilerplate'))
1 # tests.py 2 import unittest 3 import requests 4 from unittest.mock import patch 5 from main import upload 6 7 text = 'This is ran from a test case' 8 url = 'http://paste.fedoraproject.org/' 9 data = { 10 'paste_data': text, 11 'paste_lang': None, 12 'api_submit': True, 13 'mode': 'json' 14 } 15 class TestUpload(unittest.TestCase): 16 def test_upload_function(self): 17 with patch('main.requests') as mock_requests: 18 result = upload(text) # call our function 19 mock_requests.post.assert_called_once_with(url, data=data) 20 21 def test_upload_ValueError(self): 22 with patch('main.requests') as mock_requests: 23 mock_requests.post.side_effect = ValueError 24 result = upload(text) 25 mock_requests.post.assert_any_call(url, data=data) 26 self.assertEqual(result, None)
patching methods #1
>>> @patch('requests.Response')... @patch('requests.Session')... def test(session, response):... assert session is requests.Session... assert response is requests.Response... >>> test()
patching methods #2
>>> with patch.object(os, 'listdir', return_value=['abc.txt']) as mock_method:... a = os.listdir('/home/hummer')... >>> mock_method.assert_called_once_with('/home/hummer')>>>
Mock return_value>>> m = Mock()>>> m.return_value = 'some random value 4'>>> m()'some random value 4'
OR
>>> m = Mock(return_value=3)>>> m.return_value3>>> m()3
Mock side_effect
• This can be a Exception, Iterable or function.• If you pass in a function it will be called with
same arguments as the mock, unless function returns DEFAULT singleton.
#1 side_effect for Exception>>> m = Mock()>>> m.side_effect = ValueError('You are always gonna get this!!')>>> m() raise effectValueError: You are always gonna get this!!
>>> m = Mock()>>> m.side_effect = [1, 2, 3, 4]>>> m(), m(), m(), m()(1, 2, 3, 4)>>> m()StopIteration
#2 side_effect for returning sequence of values
>>> m = Mock()>>> side_effect = lambda value: value ** 3>>> m.side_effect = side_effect>>> m(2)8
#3 side_effect as function
Installation
For Python3
$ pip3 install -U pytest
For Python2
$ pip install -U pytest
or
$ easy_install -U pytest
Tests with less Boilerplate
1 import unittest 2 3 def cube(number): 4 return number ** 3 5 6 7 class Testing(unittest.TestCase): 8 def test_cube(self): 9 assert cube(2) == 8
Before py.test
1 def cube(number):2 return number ** 33 4 def test_cube():5 assert cube(2) == 867 # Here no imports or no classes are needed
After py.test
Running Tests
pytest will run all files in the current directory and its subdirectories of the form test_*.py or *_test.py or else you can always feed one file at a time.$ py.test cube.py=============================== test session starts============================================platform linux -- Python 3.4.3, pytest-2.8.3, py-1.4.30, pluggy-0.3.1rootdir: /home/hummer/Study/Nov2015PythonPune/pyt, inifile: collected 1 items
cube.py .
===============================1 passed in 0.01 seconds========================================
$ py.test
Run entire test suite
$ py.test test_bar.py
Run all tests in a specific file
$ py.test -k test_foo
Run all the tests that are named test_foo
By default pytest discovers tests in
test_*.py and *_test.py
pytest fixtures
• Fixtures are implemented in modular manner, as each fixture triggers a function call which in turn can trigger other fixtures.
• Fixtures scales from simple unit tests to complex functional test.
• Fixtures can be reused across class, module or test session scope.
1 import pytest 2 3 def needs_bar_teardown(): 4 print('Inside "bar_teardown()"') 5 6 @pytest.fixture(scope='module') 7 def needs_bar(request): 8 print('Inside "needs bar()"') 9 request.addfinalizer(needs_bar_teardown) 10 11 def test_foo(needs_bar): 12 print('Inside "test_foo()"')
[hummer@localhost fixtures] $ py.test -sv fix.py ========================= test session starts ======================================platform linux -- Python 3.4.3, pytest-2.8.3, py-1.4.30, pluggy-0.3.1 -- /usr/bin/python3cachedir: .cacherootdir: /home/hummer/Study/Nov2015PythonPune/pyt/fixtures, inifile: collected 1 items
fix.py::test_foo Inside "needs bar()"Inside "test_foo()"PASSEDInside "bar_teardown()"
========================= 1 passed in 0.00 seconds==================================[hummer@localhost fixtures] $
References
• http://www.toptal.com/python/an-introduction-to-mocking-in-python
• https://docs.python.org/dev/library/unittest.mock.html
• http://pytest.org/latest/contents.html• http://pythontesting.net/