Python Testing using Mock & PyTest
unittest.mock
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.
Mock - Basics
>>> from unittest.mock import Mock >>> m = Mock() >>> m <Mock id='140457934010912'> >>> m.some_value = 23 >>> m.some_value 23 >>> 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 cause AssertionError: 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 effect ValueError 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.forkpty m.CLD_DUMPED m.fpathconf m.CLD_EXITED m.fsdecode [CUT] m.fchown m.walk m.fdatasync m.write m.fdopen m.writev m.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_value 3 >>> 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 effect ValueError: 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
What is pytest? ● A fully featured Python Testing tool. ● Do automated tests.
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 ** 3 3 4 def test_cube(): 5 assert cube(2) == 8 6 7 # 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.1 rootdir: /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/python3 cachedir: .cache rootdir: /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/

Python testing using mock and pytest

  • 1.
  • 2.
  • 3.
    unittest.mock Defn: unittest.mock isa 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.
  • 4.
    • As adeveloper 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.
  • 5.
  • 6.
    >>> from unittest.mockimport Mock >>> m = Mock() >>> m <Mock id='140457934010912'> >>> m.some_value = 23 >>> m.some_value 23 >>> m.other_value <Mock name='mock.other_value' id='140457789462008'> Mock Objects - Basics
  • 7.
    >>> 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 cause AssertionError: Expected call: get_value(value=2) Actual call: get_value(value=42)
  • 8.
    • Flexible objectsthat 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
  • 9.
    >>> from unittest.mockimport 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 effect ValueError Customize mock objects
  • 10.
    Using spec todefine 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'
  • 11.
    Automatically create allspecs >>> from unittest.mock import create_autospec >>> import os >>> m = create_autospec(os) >>> m. Display all 325 possibilities? (y or n) m.CLD_CONTINUED m.forkpty m.CLD_DUMPED m.fpathconf m.CLD_EXITED m.fsdecode [CUT] m.fchown m.walk m.fdatasync m.write m.fdopen m.writev m.fork
  • 12.
    Using Mock throughpatch • 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.
  • 13.
    1 # main.py 2import 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'))
  • 14.
    1 # tests.py 2import 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)
  • 15.
    patching methods #1 >>>@patch('requests.Response') ... @patch('requests.Session') ... def test(session, response): ... assert session is requests.Session ... assert response is requests.Response ... >>> test()
  • 16.
    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') >>>
  • 17.
    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_value 3 >>> m() 3
  • 18.
    Mock side_effect • Thiscan 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.
  • 19.
    #1 side_effect forException >>> m = Mock() >>> m.side_effect = ValueError('You are always gonna get this!!') >>> m() raise effect ValueError: You are always gonna get this!!
  • 20.
    >>> 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
  • 21.
    >>> m =Mock() >>> side_effect = lambda value: value ** 3 >>> m.side_effect = side_effect >>> m(2) 8 #3 side_effect as function
  • 23.
    Installation For Python3 $ pip3install -U pytest For Python2 $ pip install -U pytest or $ easy_install -U pytest
  • 24.
    What is pytest? ●A fully featured Python Testing tool. ● Do automated tests.
  • 25.
    Tests with lessBoilerplate 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
  • 26.
    1 def cube(number): 2return number ** 3 3 4 def test_cube(): 5 assert cube(2) == 8 6 7 # Here no imports or no classes are needed After py.test
  • 27.
    Running Tests pytest willrun 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.1 rootdir: /home/hummer/Study/Nov2015PythonPune/pyt, inifile: collected 1 items cube.py . ===============================1 passed in 0.01 seconds========================================
  • 28.
    $ py.test Run entiretest 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
  • 29.
    pytest fixtures • Fixturesare 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.
  • 30.
    1 import pytest 2 3def 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()"')
  • 31.
    [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/python3 cachedir: .cache rootdir: /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] $
  • 32.