TDD from scratch§

If it's not tested, it's broken

Twitter: @andreacrotti

Slides: https://github.com/AndreaCrotti/ep2013

_images/wazoku.png

Dynamic language§

Python is awesome, but:

_images/noose.jpg

Typo§

Traceback (most recent call last):
  File "rare_cond.py", line 21, in <module>
    smart_function(42)
  File "rare_cond.py", line 7, in smart_function
    report_error(argO)
NameError: global name 'argO' is not defined
def smart_function(arg0):
    if rare_failure_condition():
        report_error(argO)
    else:
        normal_behaviour(arg0)

Fail§

_images/testing-goat.jpg

Misunderstanding§

['W', 'O', 'R', 'D', '1']
>>> uppercase_words("word1")
def uppercase_words(words):
    """Take a list of words and upper case them
    """
    return [w.upper() for w in words]

Does not fails, but still clearly wrong

Fail§

_images/testing-goat.jpg

Unit test§

Not a unit test§

import MySQLdb
import report


class TestReporting(unittest.TestCase):
    def setUp(self):
        dbc = MySQLdb.connect(host='host', user='user', passwd='passwd', port='port')
        cursor = dbc.cursor(MySQLdb.cursors.DictCursor)
        # insert 10 values in the db

    def test_report(self):
        rep = report.get_report()
        self.assertEqual(len(rep), 10)

Change of perspective§

how do I hack it together -> How can I prove it works

_images/lazy.jpg

Testing pure functions§

def mysum(a, b):
    return a + b

def mysubstract(a, b):
    return a - b

def test_ops():
    assert mysum(0, 0) == 0
    assert mysum(1, -1) == 0
    assert mysubstract(1, 1) == 0

def test_combined():
    a = 10
    bvals = range(100)
    for b in bvals:
        assert mysubstract(mysum(a, b), a) == b

Side effects§

In addition to returning a value, it also modifies some state or has an observable interaction with calling functions or the outside world

def silly_function(value):
    global GLOBAL_VALUE
    GLOBAL_VALUE += 1
    return (value * 2) + GLOBAL_VALUE
>>> funcs.silly_function(1)
3
>>> funcs.silly_function(1)
4

Depends on the global state -> side effect -> hard to tests

More side effects§

from time import asctime


class Report(object):
    def report(self):
        return ("at %s everything fine" % asctime())

How do I test this??

Dependency injection§

class ReportDep(object):
    def __init__(self, timefunc=asctime):
        self.timefunc = timefunc

    def report(self):
        return ("at %s everything fine" % self.timefunc())
def test_report():
    func = lambda: "now"
    assert ReportDep(func).report() == 'at now everything fine'

UGLY let's leave it to the Java guys.

Any other solution?§

library.py:

def lib_func():
    return 1000

prog.py:

import library

def dependent():
    # the lib_func can be changed at run-time
    print(library.lib_func)
    print(library.lib_func())

dependent()
library.lib_func = lambda: 42
dependent()

Mocking§

_images/mocking.jpg

Mock objects§

Mock the behaviour of an object that we don't want to run.

class ComplexObject(object):
    def method(self):
        print("Very complex and expensive")


class Obj(object):
    def __init__(self):
        self.c = ComplexObject()
        self.c.method()
fake_complex_object_auto = Mock(autospec=lib.ComplexObject)
@patch('lib.ComplexObject', new=fake_complex_object_auto)
def test_obj(self):
    v = lib.Obj()

Patching§

lib.py:

from os import listdir

def filter_dirs(pth):
    for l in listdir(pth):
        if 'x' in l:
            yield l

test_lib.py:

class TestLib(unittest.TestCase):
    @patch('lib.listdir', new=lambda x: ['one', 'two', 'x'])
    def test_filter_dirs(self):
        res = list(lib.filter_dirs('.'))
        self.assertEqual(len(res), 1)

TDD cycle§

_images/tdd.jpg

Cycle§

Make it fail§

class Queue(object):
    def __init__(self):
        self.queue = []

    def empty(self):
        return False
        # return self.queue == []


def test_queue_empty():
    q = Queue()
    assert q.empty(), "Queue is not empty in the beginning"


if __name__ == '__main__':
    test_queue_empty()

Refactoring example§

def long_crappy_function():
    """Do a bit of everything
    """
    ls_cmd = 'ls'
    p = subprocess.Popen(ls_cmd, stdout=subprocess.PIPE,
                         stderr=subprocess.PIPE)

    out, err = p.communicate()
    res = []
    for line in out:
        if 'to-match' in line:
            res.append(line)

    dbc = MySQLdb.connect(host='host', user='user',
                          passwd='passwd', port='port')
    cursor = dbc.cursor(MySQLdb.cursors.DictCursor)
    for r in res:
        cursor.execute('INSERT INTO table VALUES (%s)' % r)

Coverage§

http://nedbatchelder.com/code/coverage/

As simple as:

nosetests show_cov.py --with-cov --cov-report=html

Coverage 2§

def smart_division(a, b):
    """Run a 'smart' division
    """
    if b == 0:
        raise Exception("Can not divide by 0")

    res = a / b
    back_res = res * b

    if back_res != a:
        return a / float(b)
    else:
        return res

My setup§

Conclusion§

_images/happy.jpg

Questions?§

Twitter: @andreacrotti Slides: https://github.com/AndreaCrotti/ep2013

_images/wazoku.png