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

The Practice of TDD: tips&tricks

The Practice of TDD: tips&tricks

PyCon Nove, 2018-04-20, Florence

Antonio Cuni

April 20, 2018
Tweet

More Decks by Antonio Cuni

Other Decks in Programming

Transcript

  1. About me PyPy core dev since 2006 pdb++, cffi, vmprof,

    capnpy, ... @antocuni http://antocuni.eu antocuni (PyCon Nove) TDD tips&ticks 1 / 35
  2. About you Either: novice programmer experienced programmer but new to

    Python and/or TDD antocuni (PyCon Nove) TDD tips&ticks 2 / 35
  3. About this talk Two parts General TDD principles Useful patterns

    and tips antocuni (PyCon Nove) TDD tips&ticks 3 / 35
  4. The goal of testing Make sure that your code works

    WRONG! antocuni (PyCon Nove) TDD tips&ticks 4 / 35
  5. The goal of testing Make sure that your code works

    WRONG! antocuni (PyCon Nove) TDD tips&ticks 4 / 35
  6. The goal of testing Make sure that your code works

    Make sure that your code does NOT break antocuni (PyCon Nove) TDD tips&ticks 5 / 35
  7. Manual testing (1) Feature A Write the code Start the

    program Navigate through N steps login click on few links push a button CRASH! (repeat) antocuni (PyCon Nove) TDD tips&ticks 6 / 35
  8. Manual testing (1) Feature A Write the code Start the

    program Navigate through N steps login click on few links push a button CRASH! (repeat) antocuni (PyCon Nove) TDD tips&ticks 6 / 35
  9. Manual testing (2) Feature B Modify the code Feature B

    works! :-) (Feature A no longer works, but you don’t notice) antocuni (PyCon Nove) TDD tips&ticks 7 / 35
  10. Manual testing (2) Feature B Modify the code Feature B

    works! :-) (Feature A no longer works, but you don’t notice) antocuni (PyCon Nove) TDD tips&ticks 7 / 35
  11. Automated testing (1) Write the code for Feature A Run

    a command (~0.5 secs) py.test test_foo.py -k test_feature_A CRASH (repeat) antocuni (PyCon Nove) TDD tips&ticks 8 / 35
  12. Automated testing (2) Write the code for Feature B Run

    a command (~0.5 secs) py.test test_foo.py -k test_feature_B It works py.test test_foo.py CRASH fix Everyone is happy :-) antocuni (PyCon Nove) TDD tips&ticks 9 / 35
  13. Automated testing (2) Write the code for Feature B Run

    a command (~0.5 secs) py.test test_foo.py -k test_feature_B It works py.test test_foo.py CRASH fix Everyone is happy :-) antocuni (PyCon Nove) TDD tips&ticks 9 / 35
  14. Automated testing (3) What’s the missing piece? You have to

    write the test! It’s just a program test frameworks/runners offer a lot of help unittest, unittest2 nose “py.test“ antocuni (PyCon Nove) TDD tips&ticks 10 / 35
  15. Automated testing (3) What’s the missing piece? You have to

    write the test! It’s just a program test frameworks/runners offer a lot of help unittest, unittest2 nose “py.test“ antocuni (PyCon Nove) TDD tips&ticks 10 / 35
  16. Test Driven Development Goal: make sure that our code does

    not break What is not tested is broken (aka: Murphy’s law) Even if it’s not broken right now, it’ll eventually break antocuni (PyCon Nove) TDD tips&ticks 11 / 35
  17. Tests first Writing code when no test is failing is

    forbidden You should write just the code to make the test passing don’t cheat :-) Each test must run in isolation bonus track: VCS commit every time you write/fix a test write meaningful commit messages don’t commit if the tests are broken (unless you are sure it’s the right thing to do :-)) make you confident in your tests {hg,git} bisect antocuni (PyCon Nove) TDD tips&ticks 12 / 35
  18. Tests first Writing code when no test is failing is

    forbidden You should write just the code to make the test passing don’t cheat :-) Each test must run in isolation bonus track: VCS commit every time you write/fix a test write meaningful commit messages don’t commit if the tests are broken (unless you are sure it’s the right thing to do :-)) make you confident in your tests {hg,git} bisect antocuni (PyCon Nove) TDD tips&ticks 12 / 35
  19. TDD benefits confidence about the quality of the code easily

    spot regressions easily find by who/when/why a regression was introduced "Why the hell did I write this piece of code?" look at the commit, and the corresponding test Remove the code, and see if/which tests fail "One of my most productive days was throwing away 1000 lines of code" (Ken Thompson) "Deleted code is debugged code" (Jeff Sickel) The power of refactoring antocuni (PyCon Nove) TDD tips&ticks 13 / 35
  20. Properties of a good test It should FAIL before your

    fix write the test first, then the code Determinism NEVER write a test which fails every other run pay attention e.g. to dictionary order Easy to READ executable documentation antocuni (PyCon Nove) TDD tips&ticks 14 / 35
  21. Readability A test tells a story One feature per test

    No complex control flow Clear failures When a test fails, the poor soul looking at it should be able to understand why antocuni (PyCon Nove) TDD tips&ticks 15 / 35
  22. Factorial example Bad def test_factorial(): for n in (5, 7):

    res = 1 for i in range(1, n+1): res *= i assert factorial(n) == res antocuni (PyCon Nove) TDD tips&ticks 16 / 35
  23. Factorial example Better def test_factorial(): assert factorial(5) == 120 assert

    factorial(7) == 5040 antocuni (PyCon Nove) TDD tips&ticks 18 / 35
  24. Factorial example Best def test_factorial(): assert factorial(5) == 2 *

    3 * 4 * 5 assert factorial(7) == 2 * 3 * 4 * 5 * 6 * 7 antocuni (PyCon Nove) TDD tips&ticks 19 / 35
  25. Easy to write We are all lazy, deal with it

    The easiest to write a test, the more likely we’ll do Invest time in a proper test infrastructure Test the infrastructure as well :) antocuni (PyCon Nove) TDD tips&ticks 20 / 35
  26. test_pypy_c example Bad def test_call_pypy(tmpdir): src = """if 1: def

    factorial(n): if n in (0, 1): return 1 return n * factorial(n-1) import sys n = eval(sys.argv[1]) print factorial(n) """ pyfile = tmpdir.join("x.py") pyfile.write(src) stdout = subprocess.check_output( ["pypy", str(pyfile), "5"]) res = eval(stdout) assert res == 2*3*4*5 antocuni (PyCon Nove) TDD tips&ticks 21 / 35
  27. test_pypy_c example Still bad def execute(tmpdir, src, *args): pyfile =

    tmpdir.join("x.py") pyfile.write(src) args = ["pypy", str(pyfile)] + list(args) stdout = subprocess.check_output(args) return eval(stdout) def test_call_pypy_2(tmpdir): src = """if 1: def factorial(n): if n in (0, 1): return 1 return n * factorial(n-1) import sys n = eval(sys.argv[1]) print factorial(n) """ res = execute(tmpdir, src, "5") assert res == 2*3*4*5 antocuni (PyCon Nove) TDD tips&ticks 22 / 35
  28. test_pypy_c example Good class TestCall(PyPyTest): def test_call(self): def factorial(n): if

    n in (0, 1): return 1 return n * factorial(n-1) # res = self.run(factorial, 5) assert res == 2*3*4*5 antocuni (PyCon Nove) TDD tips&ticks 23 / 35
  29. test_pypy_c example @pytest.mark.usefixtures(’initargs’) class PyPyTest(object): @pytest.fixture def initargs(self, tmpdir): self.tmpdir

    = tmpdir def run(self, fn, *args): fnsrc = textwrap.dedent(inspect.getsource(fn)) boilerplate = textwrap.dedent(""" import sys args = map(eval, sys.argv[1:]) print {fnname}(*args) """).format(fnname=fn.__name__) src = fnsrc + boilerplate args = map(str, args) return execute(self.tmpdir, src, *args) antocuni (PyCon Nove) TDD tips&ticks 24 / 35
  30. test_pypy_c example pypy/module/pypyjit/test/model.py https://bitbucket.org/pypy/pypy/src/ 5a1232a1c2e6e01422713659c57ae3bcd5db3f70/pypy/ module/pypyjit/test_pypy_c/model.py?at=default& fileviewer=file-view-default pypy/module/pypyjit/test/test_00model.py https://bitbucket.org/pypy/pypy/src/ 5a1232a1c2e6e01422713659c57ae3bcd5db3f70/pypy/

    module/pypyjit/test_pypy_c/test_00_model.py?at= default&fileviewer=file-view-default capnpy/testing/compiler/support.py https://github.com/antocuni/capnpy/blob/master/ capnpy/testing/compiler/support.py antocuni (PyCon Nove) TDD tips&ticks 25 / 35
  31. Decoupling components Useful in general In particular for GUIs Leads

    to a better design antocuni (PyCon Nove) TDD tips&ticks 26 / 35
  32. Decoupling components class Calculator: def __init__(self, master): ... self.total =

    0 self.entered_number = 0 self.total_label_text = IntVar() self.total_label_text.set(self.total) self.add_button = ... self.subtract_button = ... self.reset_button = ... def update(self, method): if method == "add": self.total += self.entered_number elif method == "subtract": self.total -= self.entered_number else: # reset self.total = 0 self.total_label_text.set(self.total) self.entry.delete(0, END) antocuni (PyCon Nove) TDD tips&ticks 28 / 35
  33. Decoupling components class Calculator(object): def __init__(self): self.total = 0 def

    add(self, x): self.total += x def sub(self, x): self.total -= x def reset(self): self.total = 0 antocuni (PyCon Nove) TDD tips&ticks 29 / 35
  34. Decoupling components class GUI(object): def __init__(self, master): self.calculator = Calculator()

    ... self.add_button = ... self.subtract_button = ... self.reset_button = ... self.update_total() def update_total(self): self.total_label_text.set(self.calculator.total) def update(self, method): if method == "add": self.calculator.add(self.entered_number) elif method == "subtract": self.calculator.sub(self.entered_number) else: # reset self.calculator.reset() self.update_total() self.entry.delete(0, END) antocuni (PyCon Nove) TDD tips&ticks 30 / 35
  35. Decoupling components def test_Calculator(): calc = Calculator() assert calc.total ==

    0 calc.add(3) assert calc.total == 3 calc.add(5) assert calc.total == 8 calc.sub(7) assert calc.total == 1 calc.sub(10) assert calc.total == -9 calc.reset() assert calc.total == 0 antocuni (PyCon Nove) TDD tips&ticks 31 / 35
  36. Notation ops = [ MergePoint(’merge_point’, [sum, n1], []), ResOperation(’guard_class’, [n1,

    ConstAddr(node_vtable, cpu)], []), ResOperation(’getfield_gc__4’, [n1, ConstInt(ofs_value)], [v]), ResOperation(’int_sub’, [v, ConstInt(1)], [v2]), ResOperation(’int_add’, [sum, v], [sum2]), ResOperation(’new_with_vtable’, [ConstInt(size_of_node), ConstAddr(node_vtable, cpu)], [n2]), ResOperation(’setfield_gc__4’, [n2, ConstInt(ofs_value), v2], []), Jump(’jump’, [sum2, n2], []), ] antocuni (PyCon Nove) TDD tips&ticks 33 / 35
  37. Notation https://bitbucket.org/pypy/pypy/src/ 5a1232a1c2e6e01422713659c57ae3bcd5db3f70/rpython/jit/metainterp/ optimizeopt/test/test_optimizeopt.py?at=default&fileviewer= file-view-default def test_remove_guard_class_1(self): ops = """

    [p0] guard_class(p0, ConstClass(node_vtable)) [] guard_class(p0, ConstClass(node_vtable)) [] jump(p0) """ preamble = """ [p0] guard_class(p0, ConstClass(node_vtable)) [] jump(p0) """ expected = """ [p0] jump(p0) """ self.optimize_loop(ops, expected, expected_preamble=preamble) antocuni (PyCon Nove) TDD tips&ticks 34 / 35