I719 Fundamentals of Python/testing: Difference between revisions
add testing information |
No edit summary |
||
Line 1: | Line 1: | ||
= Basic Testing = | |||
In the examples, <code>app.py</code>, <code>test_app.py</code> and <code>__init__.py</code> are all in the same directory. The tests are ran with the command <code>python3 -m unittest test_app.py</code> | |||
== check the output of a function == | |||
<code>app.py</code> | |||
<source lang="python">def get_hello_world(): | |||
return "Hello World!"</source> | |||
<code>test_app.py</code> | |||
<source lang="python">import unittest | |||
from app import get_hello_world | |||
class HelloWorldTestCase(unittest.TestCase): | |||
def test_value(self): | |||
result = get_hello_world() | |||
self.assertEqual("Hello World!", result)</source> | |||
== Check the side effect of a function == | |||
<code>app.py</code> | |||
<source lang="python">class MyPrinter(): | |||
def __init__(self, value=None): | |||
self.value = value | |||
def print_value(self): | |||
print(self.value) | |||
def set_value(self, value): | |||
self.value = value</source> | |||
<code>test_app.py</code> | |||
<source lang="python">import unittest | |||
from app import MyPrinter | |||
class HelloWorldTestCase(unittest.TestCase): | |||
def test_value(self): | |||
value = "Test Value" | |||
my_printer = MyPrinter() | |||
my_printer.set_value(value) | |||
self.assertEqual(my_printer.value, value)</source> | |||
= Advanced Testing = | = Advanced Testing = | ||
Revision as of 20:47, 14 March 2017
Basic Testing
In the examples, app.py
, test_app.py
and __init__.py
are all in the same directory. The tests are ran with the command python3 -m unittest test_app.py
check the output of a function
app.py
def get_hello_world():
return "Hello World!"
test_app.py
import unittest
from app import get_hello_world
class HelloWorldTestCase(unittest.TestCase):
def test_value(self):
result = get_hello_world()
self.assertEqual("Hello World!", result)
Check the side effect of a function
app.py
class MyPrinter():
def __init__(self, value=None):
self.value = value
def print_value(self):
print(self.value)
def set_value(self, value):
self.value = value
test_app.py
import unittest
from app import MyPrinter
class HelloWorldTestCase(unittest.TestCase):
def test_value(self):
value = "Test Value"
my_printer = MyPrinter()
my_printer.set_value(value)
self.assertEqual(my_printer.value, value)
Advanced Testing
Testing the bitcoin price printer
You can convert
btc_price.py
import requests
def main():
r = requests.get('https://api.coindesk.com/v1/bpi/currentprice.json')
data = r.json()
price = data['bpi']['EUR']['rate']
print(price)
if __name__ == '__main__':
main()
to something with a testable function
btc_price.py
import requests
def get_btc_price():
r = requests.get('https://api.coindesk.com/v1/bpi/currentprice.json')
data = r.json()
price = data['bpi']['EUR']['rate']
return price
if __name__ == '__main__':
print(get_btc_price())
Writing a Unit test
in the same directory, make a file that starts with test_
. So for this example, the file is named test_btc_price.py
. Also ensure an __init__.py
file exists so you can import python modules in this directory.
├── btc_price.py ├── test_btc_price.py ├── __init__.py
`
and then write a unit test on the value
test_btc_price.py
import unittest
from btc_price import get_btc_price
class BtcPriceTestCase(unittest.TestCase):
def test_price_accurate(self):
price = get_btc_price() # The bitcoin returned changes over time!
self.assertEqual(price, '1000')
and run using python3 -m unittest btc_price_test.py
This test will fail becuase the price of bitcoins change.
so instead check that value is correct in another way.
In this example, we check that the string value is floating point, like 1,101.12
. This is not as robust as checking if the value is correct. So if the value does not change, do not do this.
btc_price_test.py
import unittest
from btc_price import get_btc_price
class BtcPriceTestCase(unittest.TestCase):
def test_price_format(self):
"""Test price is a string with correct format"""
price = get_btc_price() # The bitcoin returned changes over time!
price_without_comma = price.replace(',', '')
# if string `price_without_comma` cannot be converted to
# a float, then this line will raise an error.
# an unhandled error will cause the test to fail.
float(price_without_comma)
Testing IO dependent functions without doing the IO
In the previous Bitcoin price example, running the test also makes a HTTP request.
This means:
- our test will fail without internet
- our test will fail if the bitcoin price API changes or is down
You may want to test these things in your test. This would make our unittest more like a functional test.
You can stub out IO functions, and test if the function is correctly written, not that the price API works.
To do this with depedency inject, we must change our original code as well.
btc_price.py
import requests
# use `requests` by default, but allow another library to be used
def get_btc_price(request_library=requests):
r = request_library.get('https://api.coindesk.com/v1/bpi/currentprice.json')
data = r.json()
price = data['bpi']['EUR']['rate']
return price
if __name__ == '__main__':
print(get_btc_price())
import json
import requests
import unittest
MOCK_RESPONSE_BODY ='''
{"time":{"updated":"Mar 4, 2017 11:00:00 UTC","updatedISO":"2017-03-04T11:00:00+00:00","updateduk":"Mar 4, 2017 at 11:00 GMT"},"disclaimer":"This data was produced from the CoinDesk Bitcoin Price Index (USD). Non-USD currency data converted using hourly conversion rate from openexchangerates.org","bpi":{"USD":{"code":"USD","symbol":"$","rate":"1,284.8250","description":"United States Dollar","rate_float":1284.825},"GBP":{"code":"GBP","symbol":"£","rate":"1,044.8261","description":"British Pound Sterling","rate_float":1044.8261},"EUR":{"code":"EUR","symbol":"€","rate":"1,209.5317","description":"Euro","rate_float":1209.5317}}}
'''
class MockResponse:
@staticmethod
def json():
return json.loads(MOCK_RESPONSE_BODY)
class MockRequests:
@staticmethod
def get(url):
# This will raise an `AssertionError` if the url that the function is called with is different than expected, causing the test to fail
assert url == 'https://api.coindesk.com/v1/bpi/currentprice.json'
return MockResponse()
class BtcPriceTestCase(unittest.TestCase):
def test_price_accurate(self):
# use the stub version for the test
price = get_btc_price(request_library=MockRequests())
# The value does not change now
self.assertEqual(price, '1,209.5317')
if __name__ == '__main__':
print(get_btc_price())
That was a lot of code. Forturnately python 3 has a library for making these mock objects. See https://docs.python.org/3/library/unittest.mock.html
test_btc_price.py
import json
import unittest
from unittest.mock import Mock
MOCK_RESPONSE_BODY = '''
{"time":{"updated":"Mar 4, 2017 11:00:00 UTC","updatedISO":"2017-03-04T11:00:00+00:00","updateduk":"Mar 4, 2017 at 11:00 GMT"},"disclaimer":"This data was produced from the CoinDesk Bitcoin Price Index (USD). Non-USD currency data converted using hourly conversion rate from openexchangerates.org","bpi":{"USD":{"code":"USD","symbol":"$","rate":"1,284.8250","description":"United States Dollar","rate_float":1284.825},"GBP":{"code":"GBP","symbol":"£","rate":"1,044.8261","description":"British Pound Sterling","rate_float":1044.8261},"EUR":{"code":"EUR","symbol":"€","rate":"1,209.5317","description":"Euro","rate_float":1209.5317}}}
'''
class BtcPriceTestCase(unittest.TestCase):
def test_price_accurate(self):
# generate the mocks in the test function scope
mock_response = Mock()
mock_response.json = Mock(return_value=json.loads(MOCK_RESPONSE_BODY))
mock_requests = Mock()
mock_requests.get = Mock(return_value=mock_response)
price = get_btc_price(request_library=mock_requests)
# The value does not change now
self.assertEqual(price, '1,209.5317')
Patch
But we still had to change our function to support a changeable 'requests_library'. This is okay as well, and is normal in some languages. The practice is called Dependency Injection, and is used with object oriented design. But often in python, people will 'patch' a function when it is ran in a testing environment. This changes the body of the function in runtime.
see https://docs.python.org/3/library/unittest.mock.html#unittest.mock.patch
test_btc_price.py
import json
import unittest
from unittest.mock import Mock, patch
from btc_price import get_btc_price
MOCK_RESPONSE_BODY = '''
{"time":{"updated":"Mar 4, 2017 11:00:00 UTC","updatedISO":"2017-03-04T11:00:00+00:00","updateduk":"Mar 4, 2017 at 11:00 GMT"},"disclaimer":"This data was produced from the CoinDesk Bitcoin Price Index (USD). Non-USD currency data converted using hourly conversion rate from openexchangerates.org","bpi":{"USD":{"code":"USD","symbol":"$","rate":"1,284.8250","description":"United States Dollar","rate_float":1284.825},"GBP":{"code":"GBP","symbol":"£","rate":"1,044.8261","description":"British Pound Sterling","rate_float":1044.8261},"EUR":{"code":"EUR","symbol":"€","rate":"1,209.5317","description":"Euro","rate_float":1209.5317}}}
'''
class BtcPriceTestCase(unittest.TestCase):
def test_price_accurate(self):
# generate the mocks in the test function scope
mock_response = Mock()
mock_response.json = Mock(return_value=json.loads(MOCK_RESPONSE_BODY))
mock_requests = Mock()
mock_requests.get = Mock(return_value=mock_response)
with patch('btc_price.requests', mock_requests):
price = get_btc_price()
# The value does not change now
self.assertEqual(price, '1,209.5317')
And you can omit the keyword argument for request_library
, reverting to the original script
btc_price.py
import requests
def get_btc_price():
r = requests.get('https://api.coindesk.com/v1/bpi/currentprice.json')
data = r.json()
price = data['bpi']['EUR']['rate']
return price
if __name__ == '__main__':
print(get_btc_price())