Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Unit Testing (in Python)

91ce30388d6d552b697eb67659a371ba?s=47 Greg Price
February 27, 2013

Unit Testing (in Python)

Tests help you write better code and make changes faster with confidence they're right. With the right tools and techniques, they don't have to be a burden. We'll show how to get started quickly with Python's 'unittest' and 'nosetests' libraries, and cover techniques for making your tests test the right thing, be robust to change, and run quickly.

As presented at ConFoo, 2013-02-27.

91ce30388d6d552b697eb67659a371ba?s=128

Greg Price

February 27, 2013
Tweet

Transcript

  1. Unit Testing (in Python) Greg Price google:greg+price Solano Labs ConFoo

    2013-02-27 1 / 58
  2. Full disclosure: http://tddium.com/ 2 / 58

  3. § 1: Just so 3 / 58

  4. $ python Python 2.7.3 (default, Aug 1 2012) >>> import

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

    >>> x = mycode.Foo(3, 4) >>> x.work(5) 30 5 / 58
  6. $ 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
  7. >>> # (go off and edit the code) >>> reload(mycode);

    f() 30 7 / 58
  8. >>> def f(): ... x = mycode.Foo(3, 4) ... return

    x.work(5) ... >>> f() 30 >>> # now add x.work(15) to f()? 8 / 58
  9. $ cat test_mycode.py import mycode x = mycode.Foo(3, 4) print

    x.work(5) $ python test_mycode.py 60 9 / 58
  10. $ # (go off and edit the code) $ python

    test_mycode.py 30 $ # (go edit test_mycode.py) $ python test_mycode.py 30 30 10 / 58
  11. $ 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
  12. /§ 1: Just so 12 / 58

  13. § 2: Pytest 13 / 58

  14. unittest nosetests py.test 14 / 58

  15. py.test 15 / 58

  16. (almost) Nothing to learn 16 / 58

  17. $ 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
  18. $ cat test_minimal.py def test_good(): pass def test_bad(): assert False

    18 / 58
  19. def test_str(): x = ’sham spa ergs’ assert x ==

    ’ham spam eggs’ 19 / 58
  20. _________________ 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
  21. def test_list(): assert [1, 2] == [3, 2] 21 /

    58
  22. ________________ 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
  23. /§ 2: Pytest 23 / 58

  24. § 3: Abstraction 24 / 58

  25. Tests as data Fixtures 25 / 58

  26. Abstraction at top Abstraction at bottom 26 / 58

  27. Tests as data / Abstraction at top 27 / 58

  28. # 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
  29. 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
  30. # (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
  31. 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
  32. Fixtures / Abstraction at bottom 32 / 58

  33. # 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
  34. ____________ test_ehlo _____________ smtp = <smtplib.SMTP instance at 0x236cab8> 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
  35. # 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
  36. # 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
  37. # 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
  38. /§ 3: Abstraction 38 / 58

  39. § 4: What to Test 39 / 58

  40. Tests are code 40 / 58

  41. 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
  42. Cost of tests: writing them 42 / 58

  43. Cost of tests: maintaining them 43 / 58

  44. Pure functions + clear, stable semantics 44 / 58

  45. Pure functions + clear, stable semantics > External (⇒ stable)

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

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

    APIs Internal APIs (respect encapsulation) consider disposable test (:/) 44 / 58
  48. Test failure cases too 45 / 58

  49. Test the tests 46 / 58

  50. /§ 4: What to Test 47 / 58

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

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

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