Test Driven Development in Python using unittest and mock
Created by Gerard Keating
What is Test Driven Development?
Test Driven Development is writing tests for your code before writing your code
Test Driven Development is NOT...
- TDD is NOT a replacement for QA.
It is a software development process - TDD does NOT take a long time.
It is quick to do and will save you massive amount of time in the long run - TDD is NOT hard.
Anyone who codes can be shown how to do it
What you need?
- NOTHING! Just Python and unittest which has been part of the python standard library since version 2.1
- Except the mock library is very useful which we will show later
- And do get nose which is a good command line tool for managing/running your tests
- Also I recommend pycharm which is a great IDE for python that recently went free
Let's do an example
“Create a function that finds all files in a folder that end in .exe”
Write test first
Boilerplate Code:
import unittest
class Test_Case(unittest.TestCase):
def test_something(self):
self.assertEqual(True, False)
if __name__ == '__main__':
unittest.main()
First test should always FAIL
python test_folder_parse.py
F
======================================================================
FAIL: test_something (__main__.Test_Case)
----------------------------------------------------------------------
Traceback (most recent call last):
File "test_folder_parse.py", line 6, in test_something
self.assertEqual(True, False)
AssertionError: True != False
----------------------------------------------------------------------
Ran 1 test in 0.001s
FAILED (failures=1)
Actual Test
import unittest
import folder_parse
class Test_folder_parse(unittest.TestCase):
def test_get_exes_folder(self):
result = folder_parse.get_exes_folder("C:\\test_data_tdd_presentation")
#like assertEqual but ignores order
self.assertItemsEqual(result,
['uninst.exe', 'sarci32_2013_06_14_002_145026.exe'])
if __name__ == '__main__':
unittest.main()
FAIL again
Traceback (most recent call last):
File "test_folder_parse.py", line 3, in module
import folder_parse
ImportError: No module named folder_parse
Actually Write non-test code
folder_parse.py
import os
def get_exes_folder(folder_path):
file_names = os.listdir(folder_path)
res = []
for file_name in file_names:
if os.path.splitext(file_name)[1]==".exe":
res.append(file_name)
return res
PASS!
.
----------------------------------------------------------------------
Ran 1 test in 0.000s
OK
Setup unit test
Currently our unit tests relies on the folder C:\test_data_tdd_presentation existing which has two problems:- C:\test_data_tdd_presentation might not exist in its current state in the future or on other machines
- Accessing C:\test_data_tdd_presentation could be slow
Unit tests SHOULD BE ABLE TO RUN ANYWHERE!
Unit tests SHOULD BE FAST!
unit test setup
import unittest
import tempfile
import os
import shutil
import folder_parse
class Test_folder_parse(unittest.TestCase):
def test_get_exes_folder(self):
#setup
temp_folder = tempfile.mkdtemp()
files_to_create = ["1.exe",
"2.exe",
"3.log",
"4"]
for file_name in files_to_create:
fp = open(os.path.join(temp_folder, file_name), 'w')
fp.close()
#
result = folder_parse.get_exes_folder(temp_folder)
#like assertEqual but ignores order
self.assertItemsEqual(result,
["1.exe", "2.exe"]
)
#teardown
shutil.rmtree(temp_folder)
if __name__ == '__main__':
unittest.main()
unit test setup and teardown
import unittest
import tempfile
import os
import shutil
import folder_parse
class Test_folder_parse(unittest.TestCase):
temp_folder = None
def setUp(self):
self.temp_folder = tempfile.mkdtemp()
files_to_create = ["1.exe",
"2.exe",
"3.log",
"4"]
for file_name in files_to_create:
fp = open(os.path.join(self.temp_folder, file_name), 'w')
fp.close()
def test_get_exes_folder(self):
result = folder_parse.get_exes_folder(self.temp_folder)
self.assertItemsEqual(result, ["1.exe", "2.exe"] )
def tearDown(self):
shutil.rmtree(self.temp_folder)
if __name__ == '__main__':
unittest.main()
NOTE: setUp and tearDown is run PER test
Mocking
- Previous example still creates folders which if you have 1000s of tests could be slow
- How do you list a directory without creating a directory?
- Use mock
Mock by example
import unittest
import mock
import folder_parse
class Test_folder_parse(unittest.TestCase):
@mock.patch("os.listdir")
def test_get_exes_folder(self, mock_listdir):
#Given
mock_listdir.return_value = ["1.exe", "2.exe", "3.log", "4"]
tempfolder = "Z:\\whatever"
#When
result = folder_parse.get_exes_folder(tempfolder)
#Then
mock_listdir.assert_called_once_with(tempfolder)
self.assertItemsEqual(result, ["1.exe", "2.exe"] )
if __name__ == '__main__':
unittest.main()
Mocking
Pros:- No need for setup
- Faster
- Allows you to test how libraries and APIs are called
Cons:
- Testing implementation not behaviour
e.g. if one decided to use os.walk instead of os.listdir the test would fail/have to be rewritten - You need to know how, what is being mocked, responds
More tests
@mock.patch("os.listdir")
def test_get_exes_folder_upper_case(self, mock_listdir):
#Given
mock_listdir.return_value = ["1.exe", "2.EXE", "3.exe.log", "exe.4"]
#When
result = folder_parse.get_exes_folder("Z:\\whatever")
#Then
self.assertItemsEqual(result, ["1.exe", "2.EXE"] )
FAIL
python test_folder_parse.py
.F
======================================================================
FAIL: test_get_exes_folder_upper_case (__main__.Test_folder_parse)
----------------------------------------------------------------------
Traceback (most recent call last):
File "C:\Python27\lib\site-packages\mock.py", line 1201, in patched
return func(*args, **keywargs)
File "C:/Users/Gerard_Keating/PycharmProjects/tdd_presentation/test_folder_parse.py", line 27, in test_get_exes_folder_upper_case
self.assertItemsEqual(result, ["1.exe", "2.EXE"] )
AssertionError: Element counts were not equal:
First has 1, Second has 0: '2.EXE'
----------------------------------------------------------------------
Ran 2 tests in 0.095s
FAILED (failures=1)
Process finished with exit code 1
Fix
import os
def get_exes_folder(folder_path):
file_names = os.listdir(folder_path)
res = []
for file_name in file_names:
if os.path.splitext(file_name)[1].lower()==".exe":
res.append(file_name)
return res
PASS
python test_folder_parse.py
..
----------------------------------------------------------------------
Ran 2 tests in 0.001s
OK
Don't forget to refactor!
Current code:import os
def get_exes_folder(folder_path):
file_names = os.listdir(folder_path)
res = []
for file_name in file_names:
if os.path.splitext(file_name)[1].lower()==".exe":
res.append(file_name)
return res
import os
def get_exes_folder(folder_path):
return [fn for fn in os.listdir(folder_path) if os.path.splitext(fn)[1].lower()==".exe"]
..
----------------------------------------------------------------------
Ran 2 tests in 0.001s
OK
What if the folder does not exist?
Lets decide that if the folder does not exist our function will return None
@mock.patch("os.listdir")
def test_get_exes_folder_folder_does_not_exist(self, mock_listdir):
#Given
mock_listdir.side_effect = OSError("The system cannot find the path")
#When
result = folder_parse.get_exes_folder("Z:\\whatever")
#Then
self.assertEqual( result, None )
python test_folder_parse.py
.E.
======================================================================
ERROR: test_get_exes_folder_folder_does_not_exist (__main__.Test_folder_parse)
----------------------------------------------------------------------
Traceback (most recent call last):
File "C:\Python27\lib\site-packages\mock.py", line 1201, in patched
return func(*args, **keywargs)
File "C:/Users/Gerard_Keating/PycharmProjects/tdd_presentation/test_folder_parse.py", line 34, in test_get_exes_folder_folder_does_not_exist
result = folder_parse.get_exes_folder("Z:\\whatever")
File "C:\Users\Gerard_Keating\PycharmProjects\tdd_presentation\folder_parse.py", line 4, in get_exes_folder
return [fn for fn in os.listdir(folder_path) if os.path.splitext(fn)[1].lower()==".exe"]
File "C:\Python27\lib\site-packages\mock.py", line 955, in __call__
return _mock_self._mock_call(*args, **kwargs)
File "C:\Python27\lib\site-packages\mock.py", line 1010, in _mock_call
raise effect
OSError: The system cannot find the path
----------------------------------------------------------------------
Ran 3 tests in 0.004s
FAILED (errors=1)
FIX for folder does not exist?
import os
def get_exes_folder(folder_path):
try:
return [fn for fn in os.listdir(folder_path) if os.path.splitext(fn)[1].lower()==".exe"]
except OSError:
return None
python test_folder_parse.py
...
----------------------------------------------------------------------
Ran 3 tests in 0.001s
OK
- unit testing can be used to define more precisely what the function is supposed to do
- mocking can be used to replicate nearly any situation
Share your testing code
“Create a function that finds all files in a folder that end in .exe”
import unittest
import mock
import folder_parse
class Test_folder_parse(unittest.TestCase):
@mock.patch("os.listdir")
def test_get_exes_folder(self, mock_listdir):
#Given
mock_listdir.return_value = ["1.exe", "2.exe", "3.log", "4"]
tempfolder = "Z:\\whatever"
#When
result = folder_parse.get_exes_folder(tempfolder)
#Then
mock_listdir.assert_called_once_with(tempfolder)
self.assertItemsEqual(result, ["1.exe", "2.exe"] )
@mock.patch("os.listdir")
def test_get_exes_folder_upper_case(self, mock_listdir):
#Given
mock_listdir.return_value = ["1.exe", "2.EXE", "3.exe.log", "exe.4"]
#When
result = folder_parse.get_exes_folder("Z:\\whatever")
#Then
self.assertItemsEqual(result, ["1.exe", "2.EXE"] )
@mock.patch("os.listdir")
def test_get_exes_folder_folder_does_not_exist(self, mock_listdir):
#Given
mock_listdir.side_effect = OSError("The system cannot find the path")
#When
result = folder_parse.get_exes_folder("Z:\\whatever")
#Then
self.assertEqual( result, None )
if __name__ == '__main__':
unittest.main()
If something is hard to test it probably needs refactoring
import requests
from urllib import urlencode
def find_definition(word):
q = 'define ' + word
url = 'http://api.duckduckgo.com/?'
url += urlencode({'q': q, 'format': 'json'})
response = requests.get(url)
data = response.json()
definition = data[u'Definition']
if definition == u'':
raise ValueError('that is not a word')
return definition
Code courtesy of Brandon Rhodes pyconie 2013 talk http://rhodesmill.org/brandon/slides/2013-10-pyconie/
Clean code is easier to test
def build_url(word):
q = 'define ' + word
url = 'http://api.duckduckgo.com/?'
url += urlencode({'q': q, 'format': 'json'})
return url
def call_json_api(url):
response = requests.get(url)
data = response.json()
return data
def pluck_definition(data):
definition = data[u'Definition']
if definition == u'':
raise ValueError('that is not a word')
return definition
def find_definition(word):
url = build_url(word)
data = call_json_api(url)
return pluck_definition(data)
Clean code is easier to test example
def test_build_url():
assert build_url('word') == (
'http://api.duckduckgo.com/'
'?q=define+word&format=json')
def test_build_url_with_punctuation():
assert build_url('what?!') == (
'http://api.duckduckgo.com/'
'?q=define+what%3F%21&format=json')
def test_build_url_with_hyphen():
assert build_url('hyphen-ate') == (
'http://api.duckduckgo.com/'
'?q=define+hyphen-ate&format=json')
References and further reading
- Unittest documentation docs.python.org/2/library/unittest.html
- Mock documentation voidspace.org.uk/python/mock/
- Great talk at this year's pycon Ireland rhodesmill.org/brandon/slides/2013-10-pyconie/
- Simple tdd python article net.tutsplus.com/tutorials/python-tutorials/test-driven-development-in-python/
?QUESTIONS?
THANK YOU
Anymore questions please e-mail me gerard_keating@symantec.com
Test Driven Development
By gerardk
Test Driven Development
Test Driven Development in Python using unittest and mock
- 2,007