Test-driven development (in python)

Sam Cunliffe

comms: swc slack | github | email

Sainsbury Wellcome Centre pyClub. 2023-04-05.

Frontmatter

  • These slides are also at: https://scnlf.me/swc-pyclub-tdd
  • All of the toy code is/will be in a repo:
    git@github.com:samcunliffe/swc-pyclub-tdd.git.
  • I'm trying to do this whole thing from vscodium ๐Ÿ•ถ.
    • If I need terminal+vim you're allowed to laugh at me.
S Cunliffe, TDD @ SWC pyClub, 2023-04-05.

Who is this guy?

  • In a previous life, I wrote software for a particle physics experiment.
  • Now, I write software in ARC (UCL's central RSE team.)
  • We're collaborating with Niko (mostly) on a project with Branco and Margrie labs.
  • Niko asked me to talk to your gang.

โ€ƒ

S Cunliffe, TDD @ SWC pyClub, 2023-04-05.

๐Ÿ’™ Python, ๐Ÿ’› TDD.

  • Even though it wasn't my first, python is my favourite.
  • I probably "think" in (bad) python.
    • A lot of C++ in work.
  • I'm a fan of TDD. I've used it a fair bit.
    • I might be a bit of an evangelist.
S Cunliffe, TDD @ SWC pyClub, 2023-04-05.

History lesson and some jargon.

1996. Extreme programming (XP).
1999. ๐Ÿ“– Beck, K., Extreme Programming Explained (1st Ed)
2001. http://agilemanifesto.org/
โ€ƒโ€ƒ (Agile is "just" an approach to sw dev.)
2003. ๐Ÿ“– Beck, K., Test-Driven Development by Example (1st Ed) (O'Reilly via UCL)

"Rediscovery of TDD"

1957. ๐Ÿ“– McCracken, D. D., Digital Computer Programming.

"... it is highly desirable that the 'customer' prepare the check case ..."

S Cunliffe, TDD @ SWC pyClub, 2023-04-05.

What?

  • You're already testing code by running it interactively.
  • Perhaps you know from experience, that you shouldn't write too much code before checking it does what you want.

TDD is a formalisation of this process.

S Cunliffe, TDD @ SWC pyClub, 2023-04-05.

TDD: tests first.

You have a feature or spec. or a bug.

  1. Don't write any implementation code.
  2. Write (a) failing test(s) for the feature or reproduce the bug.
    • Absolutely needs to fail in exactly the way you expect it to.
  3. Now you may write some implementation.
    • Minimal code needed to make all tests pass.
    • Go back to 1 if you want more functionality.
  4. "Refactor". Tidy up the messy minimal code (move it or rename stuff or whatever).
S Cunliffe, TDD @ SWC pyClub, 2023-04-05.

... the same thing formulated as a rule.

You are not allowed to write any new active implementation code unless it is to make an existing test pass.



... the same thing formulated as emoji

๐Ÿ”ด๐ŸŸข๐Ÿ”ƒ

S Cunliffe, TDD @ SWC pyClub, 2023-04-05.

The cycle of TDD.

S Cunliffe, TDD @ SWC pyClub, 2023-04-05.

The cycle sawtooth of TDD.


S Cunliffe, TDD @ SWC pyClub, 2023-04-05.

Demo.

  • Wanna clone?
    git clone git@github.com:samcunliffe/swc-pyclub-tdd.git
    cd swc-pyclub-tdd
    pip install -e ".[dev]"
    
  • But you actually don't need to:
    • You could just cookiecut something with tests and a github workflow,
    • or just play along in your favourite CI-enabled current project.
S Cunliffe, TDD @ SWC pyClub, 2023-04-05.

When should you use it?

  • If you have a bug that your not testing for.
    • Write a test that reproduces the bug. I.e. fails on your main branch.
  • To implement a brand-new feature or a brand-new function.
    • Especially in a large codebase with fewer tests.
    • Especially if it's a complicated thing and you don't know how to do it yet.

Trust the process.

S Cunliffe, TDD @ SWC pyClub, 2023-04-05.

Benefits.

(In no particular order.)

  • Encourages simple code.
  • Encourages testable code in units.
  • Bake-in constant code verification.
  • Confidence.
    • Has made me look way cooler than I really am ๐Ÿคซ.
    • Collaborators will be more confident in your pull requests.
  • Encourages focus on project spec.
    • Encode use cases in the test cases.
S Cunliffe, TDD @ SWC pyClub, 2023-04-05.

When might you not use TDD?

  • Start-ups.
  • If your test suite is already comprehensive and coverage is good.

Other disadvantages.

  • False sense of confidence.
  • Test code is code too (and will need to be maintained).
    • Excessive test:code ratios. (I found a talk where the dev talks about 3โจ‰ more test code.)
  • Overzealous TDD believers ๐Ÿ˜‡.
  • Tests are too close in coding to your implementation.
S Cunliffe, TDD @ SWC pyClub, 2023-04-05.

Takehomes.

Do use TDD for bugfixing.

Do use TDD for clarifying specs.

Do use TDD for complicated/unfamiliar codebases.

Beware (anti-)implementation.

Misc. tips for testing in general.

๐Ÿง 

S Cunliffe, TDD @ SWC pyClub, 2023-04-05.

Actually make your tests tests.

(A stupid example.)

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

def test_add():
    assert add(1, 3) == 1 + 3

This ๐Ÿ‘† is not a test. No matter what the function name says.

S Cunliffe, TDD @ SWC pyClub, 2023-04-05.

Beware...

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

@pytest.mark.parametrise('a, b, res', [(1, 2, 1 + 2)])
def test_add(a, b, res):
    assert add(a, b) == res

Stupid, but not so stupid.

S Cunliffe, TDD @ SWC pyClub, 2023-04-05.

Sometimes hard-coded is OK.

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

@pytest.mark.parametrise('a, b, res', [(1, 2, 3)])
def test_add(a, b, res):
    assert add(a, b) == res

Also think lookup tables, v. small test data committed along with the code.

S Cunliffe, TDD @ SWC pyClub, 2023-04-05.

Test behavior, not implementation details.

This ๐Ÿ‘† is a vaguely famous mantra.

def test_adding_data():
    data = mylibrary.load_data("naturepaper.tsv")
    data.add(datum)
    assert data[:-1] == datum

# better - decide on the API behavior
def test_retrieving_data_added():
    """This test is robust if I change how I store `data`
     under the hood (array or df or whatever)"""
    data = mylibrary.load_data("naturepaper.tsv")
    data.add(datum)
    assert data.get(datum.title) == datum
S Cunliffe, TDD @ SWC pyClub, 2023-04-05.

Also test failures.

with pytest.raises(PreciselyTheExceptionYouAreExpecting):
    do.illegal_thing()
S Cunliffe, TDD @ SWC pyClub, 2023-04-05.

Avoid testing python itself.

@dataclass
class Neuron:
    activation_threshold: float # mV

def test_neuron_object():
    n = Neuron(-60)
    assert n.activation_threshold == pytest.approx(-60.0)

dataclasses.dataclass getters and setters probably don't need testing.

S Cunliffe, TDD @ SWC pyClub, 2023-04-05.

Appendices.

S Cunliffe, TDD @ SWC pyClub, 2023-04-05.

Some ideas (outside of TDD).

  • If you are lucky enough to have pair coding and hackdays: split up.
    • Red team writes tests, green team writes implementation.
  • If you're on your own, implement many failing tests (one per git branch?) in a chunk then go back to the first one and implement it.
  • Practice the self-discipline: write down the solution (in pencil on dead trees, if needs be), don't implement.
S Cunliffe, TDD @ SWC pyClub, 2023-04-05.

Further reading.

S Cunliffe, TDD @ SWC pyClub, 2023-04-05.

Other paradigms.

(I'm not really qualified to talk about these too much.)

  • Behaviour-driven development.
    • Similar to TDD in encoding the product specs.
    • The Turing Way says this:

      Simply put, under the test-driven development paradigm, we check โ€œhas the thing been done correctly?โ€, whereas under behaviour-driven development we test โ€œhas the correct thing been done?โ€.

  • Data-driven development.
  • Docs or didn't happen.
  • (A weird one) tutorial-driven development.
S Cunliffe, TDD @ SWC pyClub, 2023-04-05.

aesthetic vspace so the title isn't too close to the UCL banner