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
... that's it

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:
  1. C:\test_data_tdd_presentation might not exist in its current state in the future or on other machines
  2. 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
Refactored:
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 )
FAILS
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
PASS
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

?QUESTIONS?

THANK YOU

Anymore questions please e-mail me gerard_keating@symantec.com

Made with Slides.com