Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Let your test drive your development.

Let your test drive your development.

#sprk2012 talk. Described 'why does test drive a development'.

MOROHASHI Kyosuke

September 16, 2012
Tweet

More Decks by MOROHASHI Kyosuke

Other Decks in Programming

Transcript

  1. About me • MOROHASHI Kyosuke • @moro on Twitter and

    GitHub • works at @esminc • One of the authors of Rails3 Recipe Book
  2. Agenda • Some may think that tests are interrupters of

    software development. • I think tests are good drivers of development.
  3. Why write test? • Because it's RIGHT way. • Because

    it improves QUALITY. • Because it reports regression early. -- Is it only an expensive insurance for the future?
  4. run • We don't know whether a code really works

    well without running it. • More than anything, we:programmer want to run a code just after we wrote it.
  5. if __FILE__ == $0 require 'pp' myobj = MyClass.new(:arg) p

    myobj.do_something pp myobj end Imagine: Run code without test.
  6. if __FILE__ == $0 require 'pp' alice = Person.create(name: 'alice',

    age: 24) alisia = Person.create(name: 'alisia', age: 30) bob = Person.create(name: 'bob', age: 18) chris = Person.create(name: 'chris', age: 42) pp Person.name_like('ali') pp Person.younger_than(24) end get more^2 complex
  7. $ rails console > alice = Person.create(name: 'alice') > alissa

    = Person.create(name: 'alissa') > bob = Person.create(name: 'bob') > chris = Person.create(name: 'chris') > > Person.name_like('ali') > Person.younger_than(24) We have nice REPL on Rails, too.
  8. if __FILE__ == $0 require 'pp' alice = Person.create(name: 'alice',

    age: 24) alisia = Person.create(name: 'alisia', age: 30) bob = Person.create(name: 'bob', age: 18) chris = Person.create(name: 'chris', age: 42) pp Person.name_like('ali') pp Person.younger_than(24) end Data setup & teardown Junk data remains, must cleanup by hand.
  9. if __FILE__ == $0 require 'pp' alice = Person.create(name: 'alice',

    age: 24) alisia = Person.create(name: 'alisia', age: 30) bob = Person.create(name: 'bob', age: 18) chris = Person.create(name: 'chris', age: 42) pp Person.name_like('ali') pp Person.younger_than(24) end Assert by your eye Watch output on each run.
  10. if __FILE__ == $0 require 'pp' alice = Person.create(name: 'alice',

    age: 24) alisia = Person.create(name: 'alisia', age: 30) bob = Person.create(name: 'bob', age: 18) chris = Person.create(name: 'chris', age: 42) pp Person.name_like('ali') pp Person.younger_than(24) end Assertions depends on each other Must be careful to maintain data.
  11. if __FILE__ == $0 require 'pp' alice = Person.create(name: 'alice',

    age: 24) alisia = Person.create(name: 'alisia', age: 30) bob = Person.create(name: 'bob', age: 18) chris = Person.create(name: 'chris', age: 42) pp Person.name_like('ali') pp Person.younger_than(24) end Assertions depends on each other It's more painful to maintain DESTRUCTIVE method's. only 1 record needed for `name_like` execution ̇ OMG: change also younger_than result
  12. Imagine: Run code without test. • data setup & teardown.

    • assert by your eye. • Assertions depends on each other They become worse and worse along the software grows up.
  13. describe Person do context '4 people with below' do let!(:alice)

    { Person.create(name: 'alice', age: 24) } let!(:alissa){ Person.create(name: 'alissa', age: 30) } let!(:bob) { Person.create(name: 'bob', age: 18) } let!(:chris) { Person.create(name: 'chris', age: 42) } describe '.name_like' do subject { Person.name_like('ali').map(&:name) } it { should =~ %w[alice alissa] } end describe '.younger_than' do subject { Person.younger_than(24).map(&:name) } it { should =~ %w[bob] } end end end Run code with test.
  14. describe Person do context '4 people with below' do let!(:alice)

    { Person.create(name: 'alice', age: 24) } let!(:alissa){ Person.create(name: 'alissa', age: 30) } let!(:bob) { Person.create(name: 'bob', age: 18) } let!(:chris) { Person.create(name: 'chris', age: 42) } describe '.name_like' do subject { Person.name_like('ali').map(&:name) } it { should =~ %w[alice alissa] } end describe '.younger_than' do subject { Person.younger_than(24).map(&:name) } it { should =~ %w[bob] } end end end Arrange Run code with test. data setup & teardown.
  15. describe Person do context '4 people with below' do let!(:alice)

    { Person.create(name: 'alice', age: 24) } let!(:alissa){ Person.create(name: 'alissa', age: 30) } let!(:bob) { Person.create(name: 'bob', age: 18) } let!(:chris) { Person.create(name: 'chris', age: 42) } describe '.name_like' do subject { Person.name_like('ali').map(&:name) } it { should =~ %w[alice alissa] } end describe '.younger_than' do subject { Person.younger_than(24).map(&:name) } it { should =~ %w[bob] } end end end Act Run code with test. Act independently.
  16. describe Person do context '4 people with below' do let!(:alice)

    { Person.create(name: 'alice', age: 24) } let!(:alissa){ Person.create(name: 'alissa', age: 30) } let!(:bob) { Person.create(name: 'bob', age: 18) } let!(:chris) { Person.create(name: 'chris', age: 42) } describe '.name_like' do subject { Person.name_like('ali').map(&:name) } it { should =~ %w[alice alissa] } end describe '.younger_than' do subject { Person.younger_than(24).map(&:name) } it { should =~ %w[bob] } end end end Assert Run code with test. Assert result and report obviously.
  17. Test drives development by: Structuring execution •Testing framework structure your

    code execution. •Arrange a context •Act / perform inependentry •Assert result
  18. •An ordinary search query form. •name like •younger than •interest

    in at least one hobby Person - name (str) - age (int) Hobby - name (str) Interest - person_id(fk) - hobby_id(fk) 1 * * 1
  19. think code run What do you do first if you

    work with more complex issue? Programming workflow
  20. Imagine: You're thinking about.. • Is there any good API

    or gem...? • What are the preconditions and results? • At the end of all, What API I want to call from other code?
  21. • Ok, at first define ActiveRecord's scopes for build name

    and age condition. • Just use previous example :-). think: Start from easy way
  22. describe Person do context '4 people with below' do let!(:alice)

    { Person.create(name: 'alice', age: 24) } let!(:alissa){ Person.create(name: 'alissa', age: 30) } let!(:bob) { Person.create(name: 'bob', age: 18) } let!(:chris) { Person.create(name: 'chris', age: 42) } describe '.name_like' do subject { Person.name_like('ali').map(&:name) } it { should =~ %w[alice alissa] } end describe '.younger_than' do subject { Person.younger_than(24).map(&:name) } it { should =~ %w[bob] } end end end
  23. class Person < ActiveRecord::Base scope :name_like, ->(name) { where("#{table_name}.name LIKE

    ?", "#{name}%") } scope :younger_than, ->(age) { where("#{table_name}.age < ?", age.to_i) } end
  24. • Umm, name of the scope is ... `has_at_least_one_hobby'. •

    The scope takes an Array of hobbies like ` %w[programming BBQ travel]`. • Ok, I write these to spec. think: How about hobby?
  25. describe Person do context '4 people with below' do #

    snip describe '.has_at_least_one_hobby(hobbies)' do let(:hobbies) { %w[programming BBQ travel] } subject(:people) do Person.has_at_least_one_hobby(hobbies) end end end end Not good name, especially too long.
  26. • We extracted person-hobby relation as "interest". • Ruby has

    `#any?` method to check at least one element matches condition in collection. • Then what if Person.interest_in_any(hobbies) think: Is there better name?
  27. describe Person do context '4 people with below' do #

    snip describe '.interest_in_any(hobbies)' do let(:hobbies) { %w[programming BBQ travel] } subject(:people) do Person.interest_in_any(hobbies) end end end end Looks good.
  28. • At least two people required to assert behavior. •

    One matches the condition and another doesn't • Above may be written in before {}(RSpec) or setup() (Test::Unit) think: Arrange precondition.
  29. describe '.interest_in_any(hobbies)' do let(:hobbies) { %w[programming BBQ travel] } before

    do hobbies.each do |hobby_name| Hobby.create!(name: hobby_name) end Hobby.create!(name: 'baseball') alice.hobbies << Hobby.where(name: 'travel').first bob.hobbies << Hobby.where(name: 'programming').first chris.hobbies << Hobby.where(name: 'baseball').first end subject(:people) { Person.interest_in_any(hobbies) } end
  30. describe '.interest_in_any(hobbies)' do let(:hobbies) { %w[programming BBQ travel] } before

    do hobbies.each do |hobby_name| Hobby.create!(name: hobby_name) end Hobby.create!(name: 'baseball') alice.hobbies << Hobby.where(name: 'travel').first bob.hobbies << Hobby.where(name: 'programming').first chris.hobbies << Hobby.where(name: 'baseball').first end subject(:people) { Person.interest_in_any(hobbies) } it { should =~ [alice, bob] } end
  31. describe '.interest_in_any(hobbies)' do let(:hobbies) { %w[programming BBQ travel] } before

    do hobbies.each do |hobby_name| Hobby.create!(name: hobby_name) end Hobby.create!(name: 'baseball') alice.hobbies << Hobby.where(name: 'travel').first bob.hobbies << Hobby.where(name: 'programming').first chris.hobbies << Hobby.where(name: 'baseball').first end subject(:people) { Person.interest_in_any(hobbies) } it { should =~ [alice, bob] } end If the test passes, you've DONE plain-normal case.
  32. • Handle multiple condition posted from a Search form. •

    in Rails's controller, parameter is parsed into Hash with string value. • and I love form_for • Implement as ActiveModel form object. think: complex query
  33. require 'spec_helper' describe PeopleQuery do include_context \ '4 people: alice(24),

    aliass(30), bob(18), chris(42)' describe '.new(name_like: "ali", younger_than: 25)' do let(:query) { PeopleQuery.new(name_like: 'ali', younger_than: 25) } subject { query.people } it { should == [alice] } end end
  34. require 'spec_helper' describe PeopleQuery do include_context \ '4 people: alice(24),

    aliass(30), bob(18), chris(42)' describe '.new(name_like: "ali", younger_than: 25)' do let(:query) { PeopleQuery.new(name_like: 'ali', younger_than: 25) } subject { query.people } it { should == [alice] } end end Act • PeopleQuery takes multiple condition by Hash • PeopleQuery#people returns conditioned ActiveRecord::Relation think: PeopleQuery API
  35. require 'spec_helper' describe PeopleQuery do include_context \ '4 people: alice(24),

    aliass(30), bob(18), chris(42)' describe '.new(name_like: "ali", younger_than: 25)' do let(:query) { PeopleQuery.new(name_like: 'ali', younger_than: 25) } subject { query.people } it { should == [alice] } end end Arrange
  36. require 'spec_helper' describe PeopleQuery do include_context \ '4 people: alice(24),

    aliass(30), bob(18), chris(42)' describe '.new(name_like: "ali", younger_than: 25)' do let(:query) { PeopleQuery.new(name_like: 'ali', younger_than: 25) } subject { query.people } it { should == [alice] } end end Assert You've also got a goal for the step.
  37. require 'spec_helper' describe PeopleQuery do include_context \ '4 people: alice(24),

    aliass(30), bob(18), chris(42)' describe '.new(name_like: "ali", younger_than: 25)' do let(:query) { PeopleQuery.new(name_like: 'ali', younger_than: 25) } subject { query.people } it { should == [alice] } end end FAQ: How much should I do test? You may test until you get a goal.
  38. • Think: good wrapper for Person.interest_any?() • How the API

    used, maybe from controller, then user input will be parsed into Hash with string value... think: How about hobby
  39. describe '.new(name_like: "ali", hobbies: "baseball")' do include_context \ 'there are

    4 hobbies: programming, travel, BBQ and baseball' let(:query) { PeopleQuery.new(name_like: 'ali', hobbies: 'baseball travel') } before do alice.hobbies << Hobby.where(name: 'BBQ') alissa.hobbies << Hobby.where(name: 'baseball') end subject { query.people } it { should == [alissa] } end 3 condition handling with simple case is DONE.
  40. class PeopleController < ApplicationController def index @query = PeopleQuery.new(params[:q]) @people

    = @query.people end end = form_for @query, url: :people, as: 'q', method: :get do |f| = f.search_field :name_like = f.number_field :younger_than = f.search_field :hobbies = f.actions do = f.submit 'Search' %ul.people-found - @people.each do |person| %li[person, :found]= person.name
  41. Steps for test to drive development • Think how your

    object act, method name, arguments and return val. • Arrange testing context. There are many advanced technique. • Write simple assertion case.
  42. Test drives development by: being try & error canvas. •

    Write code, read it and think how call it. • Share with and think together with collaborators agains real code. • Run it and get feedback.
  43. TDD has many other approach • Many technics • Fake

    it & Triangulation. • Data generation: fixture replacement • Test doubles • Assert first approach is also welcome. • Better for too difficult not to come up with solution. • But MOST IMPORTANT thing is...
  44. http://flic.kr/p/zBdU7 Test Drives Development • run: by structuring execution. •

    think: by being try and error canvas. • Also be useful in future: support refactoring and report regression.
  45. think code run The more difficult a code you write,

    the helpful this approach can be. Programming workflow
  46. Thank you for listening. - May it be a light

    for you in dark places, when all other lights go out. - May it be a light for you in dark places, when all other lights go out.
  47. FAQ. How about cuke, T/U, shoulda or ... ? •

    It's not matter. • code > run > think loop should be test framework agnostic .