Test – Driven – Development



Test – Driven – Development

0 3


how_i_tdd_reveal.js


On Github excepttheweasel / how_i_tdd_reveal.js

Test

Driven

Development

What is this talk about?

  • Red, Green, Refactor
  • When to spike
  • When to delete tests
  • When not to test

What is this talk NOT about?

  • The testing pyramid
  • Testing strategies
  • Testing styles (unit, integration)
  • TDD != Unit testing
The recent DHH driven anti TDD tests were almost entirely about unit testing and mocking. This is unfortunate because mocking and TDD are separate ideas that were developed independently. TDD introduced in 1994. Mocks introduced in 2000.

When

Try to TDD everything

  • Encourages automation
  • Verifies assumptions
  • Front loads production challenges
TDD is useful and test isolation is useful, but they both involve making trade-offs. Unfortunately, doing them 100% of the time seems to be the best way to learn what those trade-offs are, and that can temporarily lead beginners toward extremism. TDD and isolation both break down in some situations, and learning to detect those situations in advance takes a lot of time. This is true of advanced techniques in any discipline, programming or otherwise. That is the honest, non-exaggerated, no-lies-involved truth.

The Walking Skeleton

Test the entire cycle end-to-end: build, deploy and operate.

Outside In / Domain Driven

It's useful to think as a software systems as layered.

Browser HTML CSS View Presenter Ajax Server Controller Model Worker Message Queue Database Language gets increasingly technical as you go down the layers. At the surface is the language of the application's domain. User, Playlist, click, scroll. Extend even further and you'd get disk storage, IO, sector, BIOS, register, gates.

Writing your first test

  • Start at the outer most layer
  • Write a complete "sunshine road"* test
"Sunshine road" is how Mykola a former Thoughtworker would say "happy path" We write the acceptance test using only terminology from the application’s domain, not from the underlying technologies (such as databases or web servers). This helps us understand what the system should do, without tying us to any of our initial assumptions about the implementation or complicating the test with technological details.

Example (Cucumber)

Given a Teacher is logged in
And the Teacher has a Playlist with three items
When the user copies the Playlist to the "Potions" course
Then the Playlist is available in the "Potions" course
          

{P} C {Q}

  • P = precondition
  • C = command
  • Q = postcondition

Whenever P holds of the state before the execution of C, then Q will hold afterwards or C does not terminate

{Given} When {Then}

  • Given = precondition (P)
  • When = command (C)
  • Then = postcondition (Q)

Context/Action/Outcome

Given

The setup - composed using actions you know to work.

P, i.e. your precondition. Should be assumed to work, as in the case of Hoare logic. i.e. it is an axiom - something you take for granted.

Getting it (just) right

When /the user copies the Playlist to the "Potions" course/ do
  debugger
end
          
Make sure when it fails, it provides a diagnostic that would help you diagnose the problem in the future.

Write a red test

When /the user copies the Playlist to the "Potions" course/ do
  copy_playlist_to_course 'Copied Bacon', course_name
end

def copy_playlist_to_course name, course
  open_copy_modal
  select_course_from_modal course
  enter_playlist_name name
  click_action_button
end

def open_copy_modal
  page.find('#playlist-header-name i').click
  page.find('#copy-playlist-action').click
  page.should have_css('.modal-content')
end
          

Failing "When"

"When" describes the action, but there's nothing to interact with!

Start by adding a button, dialog or any other way to interact with the application.

Then

def verify_playlist_exists playlist_name
  playlist_element(playlist_name).should be_present
end

def playlist_element playlist_name
  page.find('.playlist-nav ul li.droppable', text: playlist_name)
end
          
The postcondition.

Moving down the stack

Fill in the layers as you move down the stack.

DTSTTCPW - Do the simplest thing that could possibly work.

Avoid Mocks

Mocks are a necessary evil, but where possible you should avoid them:

  • Don't mock value objects
  • Write functions where possible
  • Stub IO

Move back up the stack

Keep going until the entire scenario is green.

Adding failure cases

When you have returned to the top.

When to delete tests

When TDDing you may end up with an over-abundance of tests.

  • Negative assertions don't belong in higher level tests.
  • If a precondition can be tested in a lower level test, avoid re-testing it in a higher level test.
  • Make sure the test adds value e.g: tests branches, adds documentation.
When removing a feature or TDDing the removal of a bug. It can be useful to write a test to assert that it has been properly removed. But there can be infinite tests that assert what the system does not do. So it can be useful to remove these once you're done.

When to spike

  • When working with a third party API.
  • When working trying to figure out how to test something.
  • When you're not sure if something is going to work.

Spikes are useful to explore designs you're unsure of.

When not to test

  • Declarations
  • Getters
  • Parts of the framework
Don't test the framework or language, you should be able to test that is already tested adequately. Simple things like getters/setters should be simple. Don't test them. Declarations should be tested on their own in an isolated way and then 'applied'. But you should go to the effort of testing them again in the place where they are applied.

Conclusion

TDD can lead to better focus. Leading to less wasted development time. More reliable software. Easier to test and grow software.