python testing using mock and pytest

32
Python Testing using Mock & PyTest

Upload: suraj-deshmukh

Post on 14-Apr-2017

319 views

Category:

Software


6 download

TRANSCRIPT

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_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

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 ** 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] $