Transcript

Electric Kool-aidAcid Testing

by Ryan Davis, Seattle.rb

Test Driven DesignThe Boring Intro

XP Practices

!"#$%&'(

)'#$#

*+%,'

)'-&

.,-//0/1

2-&'

3&-,,

4','-#'#

!%50/1

3$-/5-(5

3"#$-0/-6,'

.-7'

8'$-9+%(

!%/$0/"%"#

:/$'1(-$0%/

!%,,'7$0;'

<=/'(#+09 )'#$>?(0;'/

?';',%9&'/$

4'@-7$%(0/130&9,'

?'#01/

.-0(

.(%1(-&&0/1

Copyright Ā© 1999-2006, Ronald E Jeffries, xprogramming.comused with permission

!"#$%&'(

)'#$#

*+%,'

)'-&

.,-//0/1

2-&'

3&-,,

4','-#'#

!%50/1

3$-/5-(5

3"#$-0/-6,'

.-7'

8'$-9+%(

!%/$0/"%"#

:/$'1(-$0%/

!%,,'7$0;'

<=/'(#+09 )'#$>?(0;'/

?';',%9&'/$

30&9,'

?'#01/

.-0(

.(%1(-&&0/1

4'@-7$%(0/1

Our Focus

Copyright Ā© 1999-2006, Ronald E Jeffries, xprogramming.comused with permission

Red

Green

Refactor

TDD Mantra

A Very Quick Intro to TDD

ā€¢ A Special Thanks to Steven Baker:

ā€¢ He nailed it at Canada on Rails.

ā€¢ Rewriting these slides felt like a violation of DRY.

Redclass AccountTest < Test::Unit::TestCase def test_new account = Account.new assert_equal 0, account.balance endend

E

1) Error:test_new:NameError: uninitialized constant Account ./test/unit/account_test.rb:6:in `test_new'

1 tests, 0 assertions, 0 failures, 1 errors

class Account < ActiveRecord::Baseend

# ...plus a migration with balance field

Green

.Finished in 0.055268 seconds.

1 tests, 1 assertions, 0 failures, 0 errors

Redclass AccountTest < Test::Unit::TestCase def test_deposit account = Account.new account.deposit(10) assert_equal 10, account.balance endend

.E 1) Error:test_deposit(DepositTest):NameError: undefined method `deposit' for #<Account...>./test/unit/account_test.rb:12:in `test_deposit'

2 tests, 1 assertions, 0 failures, 1 errors

Redclass Account < ActiveRecord::Base def deposit(amount)

end end

.F

1) Failure:test_deposit:<10> expected but was<0>.

2 tests, 2 assertions, 1 failures, 0 errors

Greenclass Account < ActiveRecord::Base def deposit(amount) balance += amount save endend

..Finished in 0.242602 seconds.

2 tests, 2 assertions, 0 failures, 0 errors

Refactor

..Finished in 0.253956 seconds.

2 tests, 2 assertions, 0 failures, 0 errors

class AccountTest < Test::Unit::TestCase def setup @account = Account.new end def test_new assert_equal 0, @account.balance end def test_deposit @account.deposit(10) assert_equal 10, @account.balance endend

It really is that simple...

End of Boring Stuff

Yay!

TDD as a Mind-Altering Drug

0 0.5 1 1.5 2 2.5 3 3.5 4 4.5 5

0.8

1.6

2.4

3.2

4

4.8

5.6

Mushrooms

0 0.5 1 1.5 2 2.5 3 3.5 4 4.5 5

0.8

1.6

2.4

3.2

4

4.8

5.6

TDD

Stages of TDD

Onset .5-2 hours

Coming Up 4-8 weeks

Plateau as long as used

Coming Down 2-3 months

After Effects years...

ā€¢ Shedding years of bad habits.

ā€¢ Simplified logic and data structures.

ā€¢ Not worrying about ā€œfeaturitisā€.

ā€¢ Taking your time, but being bolder.

ā€¢ Less brain clutter.

ā€¢ Less stress, more fun.

ā€¢ Expanded consciousness.

Symptoms

Onset

ā€¢ This is one of the hardest phases.

ā€¢ TDD is hard to accept for many.

ā€¢ Years of bad habits, schooling, peers, and environment make it harder.

Coming Up

ā€¢ For a while, you will be less efficient.

ā€¢ Your brain will hurt going through ā€œformal design withdrawlā€.

ā€¢ Dreaming in UML only lasts a few days.

ā€¢ Most of your bad habits are actually pretty easy to drop.

Plateau, Indications:

ā€¢ Euphoria, confidence, freedom:

ā€¢ Clarity of thought and purpose.

ā€¢ Large goals are no longer intimidating.

ā€¢ More brain is available after dropping years worth of bad habits.

ā€¢ Paranoia when maintaining non-TDD code.

ā€¢ Allergic reaction to ā€œBig Designā€.

Coming Down& After Effects

ā€¢ Lets not bother, shall we?

ā€¢ It gets grisly.

Contraindications?

PHBā€™s Coding Cowboys

ā€œPut Testing Where It Belongs: At the Endā€

Waterfall 2006 Keynote

by Brian Marick(fortunately, this example is a parody)

(unfortunately, it is too real to be funny)

Traditionalists

The ā€œDo the Simplest Thing That Could

Possibly Workā€ Mindset

ā€œIf it is not useful or necessary, free yourself from imagining that you need to make it.ā€

Shaker Proverb

ā€œCode simply so that others may simply code.ā€TDD Proverb, sorta

This One is Hard

ā€¢ Nerds resist this. I suspect humans do too.

ā€¢ For your task right now, only do the simplest thing that will work, e.g.:

ā€¢ Use an array, or a hash, or a string. Not a red-black tree, or a database, or a file.

ā€¢ Donā€™t do anything more. Really.

Why?

You Ainā€™t Gonna Need It(YAGNI)

ā€¢ Only do what you need, for right now, to pass your test and move on.

ā€¢ You ainā€™t gonna need that extra mess of complexity you are dreaming up in the back of your brain.

ā€¢ When you really need it, implement it then.

ā€¢ Basically: let all things flow organically.

Benefits of YAGNI

ā€¢ You get done sooner.

ā€¢ Your work is easier to communicate and understand.

ā€¢ Refactoring is easier.

ā€¢ Tests are easier to write.

ā€¢ You feel less stress.

(c2.com)

My Favorite Nerd Quote

ā€œMore computing sins are committed in the name of efficiency

(without necessarily achieving it) than for any other single reasonā€”

including blind stupidity.ā€W.A. Wulf

If That Wasnā€™t Clear:

ā€œRules of Optimization:Rule 1: Don't do it.

Rule 2 (for experts only): Don't do it yet.ā€M.A. Jackson

"We should forget about small efficiencies, say about 97% of the time:

premature optimization is the root of all evil."Donald Knuth

"Optimization hinders evolution."Anon.

Time#yesterday

How would you implement it?

def minutes self * 60end

def hours self * 60.minutesend

module ActiveSupport module CoreExtensions module Numeric module Time alias :day :days end end endend

module ActiveSupport module CoreExtensions module Time module Calculations module ClassMethods def yesterday self.ago(1.day) end end end end endend

def ago(seconds) seconds.until(self)end

alias :until :ago

def ago(time = ::Time.now) time - selfend

def days self * 24.hoursend

now = Time.nownow.yesterday=> now.ago(1.day)=> now.ago(1 * 24.hours)=> now.ago(1 * 24 * 60.minutes)=> now.ago(1 * 24 * 60 * 60)=> now.ago(86400)=> 86400.until(now)=> 86400.ago(now)=> now - 86400=> yesterday's time

The Rails Wayā„¢

now = Time.nownow.yesterday=> now - SEC_PER_DAY=> new - 86400=> yesterday's time

The TDD Wayā„¢class Time SEC_PER_DAY = 86400

def yesterday self - SEC_PER_DAY endend

Expand Your Mind...by not filling it

ā€¢ TDD/YAGNI is a continuous feedback loop.

ā€¢ Focusing on simplicity sheds bad habits.

ā€¢ Shedding bad habits lets you focus more on simplicity.

ā€¢ Clarity and simplicity increase your effectiveness.

ā€¢ This goes far beyond code and tests.

ā€¢ This is the mind-expansion I allude to.

Writing Good Tests

Keep it Simple!

ā€¢ YAGNI applies just as much (if not more) to tests as to code!

ā€¢ It helps keep your head clear.

ā€¢ It helps keep your goals clear.

ā€¢ It helps keep your designs solid.

Tests First

ā€¢ Write the simplest test that expresses your desire.

ā€¢ Watch it fail. (Red!)

ā€¢ Write the simplest implementation possible.

ā€¢ Watch it pass. (Green!)

ā€¢ Refactor!

ā€¢ Repeat until out of coffee (or done).

Cover Your Bases

ā€¢ Everything but stupid accessors need a test.

ā€¢ Make methods public when testing.

ā€¢ Test those private methods too!

ā€¢ Better, donā€™t have private methods.

(Pretty m

uch)

Cover Edge Cases

ā€¢ You canā€™t possibly test for every value, let alone every combination of every value.

ā€¢ Edge cases are the important values:

ā€¢ Ā±1, 0, Ā±max, today, expiration date, etc.

ā€¢ Test edge cases for parameters AND state.

ā€¢ Test both good input and bad.

ā€¢ Ensure errors are handled gracefully.

Refactor Your Tests

ā€¢ Tests are code too.

ā€¢ Keep ā€˜em clean and lean.

ā€¢ Factor duplicate code into setup, teardown, and utility methods.

Profile Your Tests

ā€¢ Slow tests donā€™t get run. Period.

ā€¢ Usually, the tests arenā€™t slow, the impl is.

ā€¢ Fixing bottlenecks through testing benefits everyone.

ā€¢ The tests keep being run, and the code gets faster!

Simple Organization

ā€¢ 1:1 mapping between files, classes/testsaccount.rbtest_account.rb

ā€¢ 1:many mapping between impl/test methods, separate by edge case:test_withdrawal (the positive case)test_withdrawal_junk (bad input)test_withdrawal_overdrawn

ā€¢ Edge cases should be separate to let you detect patterns in failures.

ā€¢ Suite of 5 tools for TDD with Ruby:

ā€¢ zentest - Audit tests.

ā€¢ multiruby - Validate multiple versions.

ā€¢ Test::Rails - Enhance rails testing.

ā€¢ unit_diff - Clarify errors.

ā€¢ autotest - ADD TDD.

Our Secret Weapon: ZenTest

& ./test.rb... 1) Failure:test_conditional4(TestTypeChecker) [./r2ctestcase.rb:1068:in `test_conditional4' ./r2ctestcase.rb:1055:in `test_conditional4']:<t(:if, t(:call, t(:lit, 42, Type.long), :==, t(:arglist, t(:lit, 0, Type.long)), Type.bool), t(:return, t(:lit, 2, Type.long), Type.void), t(:if, t(:call, t(:lit, 42, Type.long), :>, t(:arglist, t(:lit, 0, Type.long)), Type.bool), t(:return, t(:lit, 3, Type.long), Type.void), t(:return, t(:lit, 4, Type.long), Type.void), Type.void), Type.void)> expected but was<t(:if, t(:call,

unit_diffClarify Errors

Which would you rather read/debug???

This:

& ./test.rb | unit_diff... 1) Failure:test_conditional4(TestTypeChecker)12c10< :>,---> :<,

Or this:

unit_diff in Action

autotestADD TDD

ā€¢ Intelligent Fast Feedback - run the right tests every time you save.

ā€¢ Focus on Failures - reruns failed tests until they pass.

ā€¢ Fully pluggable for hours of fun.

Run

all

tests

Run

changed

tests

Run

failures +

changes

AutoTest in Action

Recommended Books

Extreme Programming Explained and Test Driven DevelopmentKent Beck

Working Effectively with Legacy CodeMichael Feathers

RefactoringMartin Fowler

A Pattern Language and A Timeless Way of BuildingChristopher Alexander

Thank You

http://www.zenspider.com/seattle.rb


Recommended