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.