Skip to content

Instantly share code, notes, and snippets.

@amandamunoz
Last active September 11, 2020 20:32
Show Gist options
  • Save amandamunoz/461923ff27329851d541cd60ec7c4d46 to your computer and use it in GitHub Desktop.
Save amandamunoz/461923ff27329851d541cd60ec7c4d46 to your computer and use it in GitHub Desktop.

TDD: Where did it all go wrong?

problems encountered w/ tdd:

  • test suites become difficult and expensive to own + maintain
  • more test code (3x more) than implementation code
  • breaking tests when refactoring code
    • particularly heavily mocked tests would break
    • tests became an obstacle for changing code
  • people started advocated for ditching TDD - "too slow", other args
  • difficult to understand intent (failures and not clear why)
  • high level tests are red for most of the time white developing (until the end)
    • high cost to maintain
    • devs start to ignore b/c they don't see value

"TDD is dead, long live TDD" - DHH/Fowler/Beck have discussion

Where did it all go wrong? --> TDD: rebooted

TDD works, we've added things to it over time that have made it bad.
TDD by Example - still the best TDD book, by Kent Beck

Avoid implementation details. Test behaviors

A key thing people get wrong
Classic example:
adding a new method is a trigger for a new test.
This is incorrect.
Test-per-class (or per method) fails the capture to ethos of TDD
Adding a class (or method) is not a trigger for writing a test

The trigger is implementing a requirement.
Nothing more, nothing less.

"I want to software to do ___" (send a newsletter, whatever )
NOT "I want software to implement a method that takes in x and y and returns z" ^ this is an implementation detail. The requirement (ticket) did not give us that.

Test the public api*
not necessarily http api here - this is the public exposed surface area being consumed
This is stable. it will not change rapidly. Software offers a stable contract to its consumers - this is what should be tested
Implementation details are unstable and will change rapidly.
Write tests to cover the use case/user story - use given/when/then

The SUT (system under test) is not a class
SUT is 'exported' from a module

Unit tests

The original definition of a unit test is the test of a module - black box - the thing that is exposed
The term unit has become fixed on a class
This leads to heavy mocking and overspecifying tests
The focus should be on a higher level
A class may be a facade, but many classes are implementation details to a module.
Implementation details change, don't write tests for them
Write tests again any stable contracts of the API
This is a key part of refactoring: separating things into what should be tested and what shouldn't
Only write tests to cover the implementation details when you need to better understand the implementation - delete them after ("developer tests", "scaffold tests")

BDD originally defined by Dan North ~2005
WHen we rwite a test, we are telling a story about how it looks from the outside
Your test is the first consumer of your code

Beck's original def of a unit test - test that runs in a unit of isolation from other tests. the test should be able to run in isolation from other tests (not depend on each other).
The unit of isolation is the TEST not the code

There are valid reasons to avoid certain things (e.g. DB, filsesystem)

  • They may impact other tests (changes to DB may impact other tests, thus necessitating a lightweight in-memory db)
  • speed
  • NOT for testing a unit of CODE in isolation of its dependencies

Focusing on testing methods creates tests that are hard to maintain

  • we don't capture behavior we want to preserve
  • we can't refactor easily

Red/Green/Refactor

Red: write a failing test (might not even compile)
Green: write the simplest thing to get the test to pass. write sinful code.
Refactor: apply design patterns, clean up code smells
** Refactoring is key!!!

Red step

Have a starting point

Green step

Commit sins.
Understand how to solve the problem to satisfy the behavior
copy/paste from stack overflow/other part of the code
Be sinful. Write bad code
Speed trumps design at this stage for this brief moment
You can't understand the solution to the problem AND engineer good code at the same time.

Refactor step

This is where you make the code good
Remove duplication, look for code smells, apply design patterns
Change design during this step, NOT behavior
DO NOT write new tests

"Dependency [not duplication] is the key problem in software" - beck
Allows us the refactor without changing tests

don't test internals
don't make this public/export them just to test them

Code smells
"Refactoring" book by fowler
Fowler defines refactoring as "not altering the external behavior, but improving internal structure"

Ice cream code testing anti-pattern - lots of manual testing https://alisterbscott.com/kb/testing-pyramids/
We instead want to testing pyramid (or testing trophy)


Some e2e testing tools (selenium etc) were overcompensating for method based testing.
Fix the problem at its source instead of relying on "expensive" tools

Mocks can be a useful tool when a resource is expensive to create, should NOT be used as a tool to isolate classes

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment