Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Python Testing using Mock and Pytest

Suraj
November 29, 2015

Python Testing using Mock and Pytest

November 29th 2015 PythonPune Meetup

Suraj

November 29, 2015
Tweet

More Decks by Suraj

Other Decks in Technology

Transcript

  1. 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.
  2. • 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.
  3. >>> 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
  4. >>> 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)
  5. • 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
  6. >>> 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
  7. 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'
  8. 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
  9. 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.
  10. 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'))
  11. 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)
  12. patching methods #1 >>> @patch('requests.Response') ... @patch('requests.Session') ... def test(session,

    response): ... assert session is requests.Session ... assert response is requests.Response ... >>> test()
  13. 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') >>>
  14. 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
  15. 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.
  16. #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!!
  17. >>> 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
  18. >>> m = Mock() >>> side_effect = lambda value: value

    ** 3 >>> m.side_effect = side_effect >>> m(2) 8 #3 side_effect as function
  19. Installation For Python3 $ pip3 install -U pytest For Python2

    $ pip install -U pytest or $ easy_install -U pytest
  20. 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
  21. 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
  22. 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========================================
  23. $ 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
  24. 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.
  25. 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()"')
  26. [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] $