Slide 1

Slide 1 text

Unit Testing (in Python) Greg Price google:greg+price Solano Labs ConFoo 2013-02-27 1 / 58

Slide 2

Slide 2 text

Full disclosure: http://tddium.com/ 2 / 58

Slide 3

Slide 3 text

§ 1: Just so 3 / 58

Slide 4

Slide 4 text

$ python Python 2.7.3 (default, Aug 1 2012) >>> import mycode >>> x = mycode.Foo(3, 4) >>> x.work(5) 60 4 / 58

Slide 5

Slide 5 text

>>> # (go off and edit the code) >>> reload(mycode) >>> x = mycode.Foo(3, 4) >>> x.work(5) 30 5 / 58

Slide 6

Slide 6 text

$ python Python 2.7.3 (default, Aug 1 2012) >>> import mycode >>> def f(): ... x = mycode.Foo(3, 4) ... return x.work(5) ... >>> f() 60 6 / 58

Slide 7

Slide 7 text

>>> # (go off and edit the code) >>> reload(mycode); f() 30 7 / 58

Slide 8

Slide 8 text

>>> def f(): ... x = mycode.Foo(3, 4) ... return x.work(5) ... >>> f() 30 >>> # now add x.work(15) to f()? 8 / 58

Slide 9

Slide 9 text

$ cat test_mycode.py import mycode x = mycode.Foo(3, 4) print x.work(5) $ python test_mycode.py 60 9 / 58

Slide 10

Slide 10 text

$ # (go off and edit the code) $ python test_mycode.py 30 $ # (go edit test_mycode.py) $ python test_mycode.py 30 30 10 / 58

Slide 11

Slide 11 text

$ cat test_mycode.py import mycode def test_work(): x = mycode.Foo(3, 4) assert x.work(5) == 30 $ py.test -q collected 1 items . 1 passed in 0.00 seconds 11 / 58

Slide 12

Slide 12 text

/§ 1: Just so 12 / 58

Slide 13

Slide 13 text

§ 2: Pytest 13 / 58

Slide 14

Slide 14 text

unittest nosetests py.test 14 / 58

Slide 15

Slide 15 text

py.test 15 / 58

Slide 16

Slide 16 text

(almost) Nothing to learn 16 / 58

Slide 17

Slide 17 text

$ cat test_mycode.py import mycode def test_work(): x = mycode.Foo(3, 4) assert x.work(5) == 30 $ py.test -q collected 1 items . 1 passed in 0.00 seconds 17 / 58

Slide 18

Slide 18 text

$ cat test_minimal.py def test_good(): pass def test_bad(): assert False 18 / 58

Slide 19

Slide 19 text

def test_str(): x = ’sham spa ergs’ assert x == ’ham spam eggs’ 19 / 58

Slide 20

Slide 20 text

_________________ test_str _________________ def test_str(): x = ’sham spa ergs’ > assert x == ’ham spam eggs’ E assert ’sham spa ergs’ == ’ham spam eggs’ E - sham spa ergs E ? - ^ E + ham spam eggs E ? + ^ test_diffs.py:7: AssertionError 20 / 58

Slide 21

Slide 21 text

def test_list(): assert [1, 2] == [3, 2] 21 / 58

Slide 22

Slide 22 text

________________ test_list _________________ def test_list(): > assert [1, 2] == [3, 2] E assert [1, 2] == [3, 2] E At index 0 diff: 1 != 3 test_diffs.py:11: AssertionError 22 / 58

Slide 23

Slide 23 text

/§ 2: Pytest 23 / 58

Slide 24

Slide 24 text

§ 3: Abstraction 24 / 58

Slide 25

Slide 25 text

Tests as data Fixtures 25 / 58

Slide 26

Slide 26 text

Abstraction at top Abstraction at bottom 26 / 58

Slide 27

Slide 27 text

Tests as data / Abstraction at top 27 / 58

Slide 28

Slide 28 text

# From PyPy test suite class FfiCallTests(object): def _run(self, atypes, avalues): # ... def fake_call(...): # ... for avalue in avalues: assert rffi.cast(TYPE, data)[0] == avalue # ... def f(): # ... fake_call(...) f() # ... total 58 lines ... 28 / 58

Slide 29

Slide 29 text

class FfiCallTests(object): def _run(self, atypes, rtype, avalues, rvalue): cif_description = get_description(atypes, rtype) def verify(*args): assert args == tuple(avalues) return rvalue FUNC = lltype.FuncType([lltype.typeOf(avalue) for avalue in avalues], lltype.typeOf(rvalue)) func = lltype.functionptr(FUNC, ’verify’, _callable=verify) func_addr = rffi.cast(rffi.VOIDP, func) for i in range(len(avalues)): cif_description.exchange_args[i] = (i+1) * 16 cif_description.exchange_result = (len(avalues)+1) * 16 unroll_avalues = unrolling_iterable(avalues) @jit.oopspec("libffi_call(cif_description,func_addr,exchange_buffer)") def fake_call(cif_description, func_addr, exchange_buffer): ofs = 16 for avalue in unroll_avalues: TYPE = rffi.CArray(lltype.typeOf(avalue)) data = rffi.ptradd(exchange_buffer, ofs) assert rffi.cast(lltype.Ptr(TYPE), data)[0] == avalue ofs += 16 if rvalue is not None: write_rvalue = rvalue else: write_rvalue = 12923 # ignored TYPE = rffi.CArray(lltype.typeOf(write_rvalue)) data = rffi.ptradd(exchange_buffer, ofs) rffi.cast(lltype.Ptr(TYPE), data)[0] = write_rvalue # (cont’d) 29 / 58

Slide 30

Slide 30 text

# (cont’d) def f(): exbuf = lltype.malloc(rffi.CCHARP.TO, (len(avalues)+2) * 16, flavor=’raw’, zero=True) ofs = 16 for avalue in unroll_avalues: TYPE = rffi.CArray(lltype.typeOf(avalue)) data = rffi.ptradd(exbuf, ofs) rffi.cast(lltype.Ptr(TYPE), data)[0] = avalue ofs += 16 fake_call(cif_description, func_addr, exbuf) if rvalue is None: res = 654321 else: TYPE = rffi.CArray(lltype.typeOf(rvalue)) data = rffi.ptradd(exbuf, ofs) res = rffi.cast(lltype.Ptr(TYPE), data)[0] lltype.free(exbuf, flavor=’raw’) return res res = f() assert res == rvalue or (res, rvalue) == (654321, None) res = self.interp_operations(f, []) assert res == rvalue or (res, rvalue) == (654321, None) self.check_operations_history(call_may_force=0, call_release_gil=1) 30 / 58

Slide 31

Slide 31 text

class FfiCallTests(object): def _run(self, atypes, avalues): # [58 lines] def test_simple_call(self): self._run([types.signed] * 2, [456, 789]) def test_simple_call_float(self): self._run([types.double] * 2, [4.5, 6.7]) def test_many_arguments(self): for i in [0, 6, 20]: self._run([types.signed] * i, [-12345*j for j in range(i)]) 31 / 58

Slide 32

Slide 32 text

Fixtures / Abstraction at bottom 32 / 58

Slide 33

Slide 33 text

# example from pytest docs import pytest @pytest.fixture def smtp(): import smtplib return smtplib.SMTP("merlinux.eu") def test_ehlo(smtp): response, msg = smtp.ehlo() assert response == 250 assert "merlinux" in msg assert 0 # for demo purposes 33 / 58

Slide 34

Slide 34 text

____________ test_ehlo _____________ smtp = def test_ehlo(smtp): response, msg = smtp.ehlo() assert response == 250 assert "merlinux" in msg > assert 0 # for demo purposes E assert 0 test_smtpsimple.py:12: AssertionError 34 / 58

Slide 35

Slide 35 text

# adapted from Topaz, a Ruby interpreter # tests/conftest.py import pytest @pytest.fixture(scope=’session’) def space(): # Building a space is expensive, # so create it just once. from topaz.objspace import ObjectSpace return ObjectSpace() 35 / 58

Slide 36

Slide 36 text

# Topaz various tests def test_addition(self, space): w_res = space.execute("return 1 + 2") assert space.int_w(w_res) == 3 def test_lambda(self, space): w_res = space.execute(""" l = lambda { |x| 3 } return [l.class, l.lambda?] """) w_cls, w_lambda = space.listview(w_res) assert w_cls is space.w_proc assert w_lambda is space.w_true 36 / 58

Slide 37

Slide 37 text

# Topaz tests/conftest.py import pytest @pytest.fixture(scope=’session’) def cached_space(): from topaz.objspace import ObjectSpace return ObjectSpace() @pytest.fixture def space(cached_space): # Building a space is expensive, so create # one once, and then just deepcopy it. return copy.deepcopy(cached_space) 37 / 58

Slide 38

Slide 38 text

/§ 3: Abstraction 38 / 58

Slide 39

Slide 39 text

§ 4: What to Test 39 / 58

Slide 40

Slide 40 text

Tests are code 40 / 58

Slide 41

Slide 41 text

webapp$ sloccount spec/ SLOC Directory SLOC-by-Language 7709 models ruby=7709 1128 controllers ruby=1128 [...] Total Physical SLOC = 13,454 Estimated Person-Months = 36.77 Total Estimated Cost = $413,938 41 / 58

Slide 42

Slide 42 text

Cost of tests: writing them 42 / 58

Slide 43

Slide 43 text

Cost of tests: maintaining them 43 / 58

Slide 44

Slide 44 text

Pure functions + clear, stable semantics 44 / 58

Slide 45

Slide 45 text

Pure functions + clear, stable semantics > External (⇒ stable) APIs 44 / 58

Slide 46

Slide 46 text

Pure functions + clear, stable semantics > External (⇒ stable) APIs Internal APIs (respect encapsulation) 44 / 58

Slide 47

Slide 47 text

Pure functions + clear, stable semantics > External (⇒ stable) APIs Internal APIs (respect encapsulation) consider disposable test (:/) 44 / 58

Slide 48

Slide 48 text

Test failure cases too 45 / 58

Slide 49

Slide 49 text

Test the tests 46 / 58

Slide 50

Slide 50 text

/§ 4: What to Test 47 / 58

Slide 51

Slide 51 text

(CC0, “Hans” on Pixabay) 48 / 58

Slide 52

Slide 52 text

(CC-BY, “soham pablo” on Flickr) 49 / 58

Slide 53

Slide 53 text

End http://pytest.org/ http://tddium.com/ 50 / 58