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. Let your test
    drive your development
    Kyosuke MOROHASHI

    View Slide

  2. View Slide

  3. About me
    • MOROHASHI Kyosuke
    • @moro on Twitter and GitHub
    • works at @esminc
    • One of the authors of
    Rails3 Recipe Book

    View Slide

  4. View Slide

  5. View Slide

  6. View Slide

  7. =begin
    PR about us

    View Slide

  8. View Slide

  9. View Slide

  10. Web built a nice paste service.
    https://www.copi.pe/

    View Slide

  11. It's a best way to share small
    confidential "pastes"within your organization.

    View Slide

  12. You can paste both text snippet and picture.

    View Slide

  13. Available in beta
    https://www.copi.pe/

    View Slide

  14. View Slide

  15. =end

    View Slide

  16. Let your test
    drive your development
    ςετʹ։ൃΛ
    ۦಈ͍ͤͨ͞

    View Slide

  17. Agenda
    • Some may think that tests are interrupters
    of software development.
    • I think tests are good drivers of
    development.

    View Slide

  18. 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?

    View Slide

  19. http://flic.kr/p/zBdU7
    Test drives
    Development
    It's not only skill, manner or RIGHT way
    but simply a driver for programming.

    View Slide

  20. think
    code
    run
    What we do when we code?
    Programming workflow

    View Slide

  21. think
    code
    run
    First of all, we want to run the code.

    View Slide

  22. 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.

    View Slide

  23. if __FILE__ == $0
    require 'pp'
    myobj = MyClass.new(:arg)
    p myobj.do_something
    pp myobj
    end
    Imagine:
    Run code without test.

    View Slide

  24. % irb -r ./my_class
    > my_obj = MyClass.new
    > myobj.do_something
    > myobj
    OK, we have nice REPL

    View Slide

  25. 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

    View Slide

  26. $ 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.

    View Slide

  27. 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.

    View Slide

  28. 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.

    View Slide

  29. 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.

    View Slide

  30. 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

    View Slide

  31. 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.

    View Slide

  32. A testing framework
    helps to
    •Arrange a context
    •Act independently
    •Assert result automatically

    View Slide

  33. 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.

    View Slide

  34. 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.

    View Slide

  35. 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.

    View Slide

  36. 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.

    View Slide

  37. Test drives development by:
    Structuring execution
    •Testing framework structure your
    code execution.
    •Arrange a context
    •Act / perform inependentry
    •Assert result

    View Slide

  38. a slightly more
    complex example
    http://flic.kr/p/5xKFpY

    View Slide

  39. An ordinary search query form
    with ordinary tables.

    View Slide

  40. •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

    View Slide

  41. think
    code
    run
    What do you do first
    if you work with more complex issue?
    Programming workflow

    View Slide

  42. think
    code
    run
    Obviously, you think. Test support it.
    Programming workflow

    View Slide

  43. 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?

    View Slide

  44. • Ok, at first define ActiveRecord's scopes
    for build name and age condition.
    • Just use previous example :-).
    think:
    Start from easy way

    View Slide

  45. 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

    View Slide

  46. 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

    View Slide

  47. • 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?

    View Slide

  48. 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.

    View Slide

  49. • 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?

    View Slide

  50. 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.

    View Slide

  51. • 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.

    View Slide

  52. 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

    View Slide

  53. • Hmm, then result should be ...
    think:
    Assert result

    View Slide

  54. 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

    View Slide

  55. http://flic.kr/p/4rxk3X
    You got a
    first GOAL

    View Slide

  56. 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.

    View Slide

  57. View Slide

  58. • 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

    View Slide

  59. 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

    View Slide

  60. 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

    View Slide

  61. 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

    View Slide

  62. 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.

    View Slide

  63. http://flic.kr/p/4rxk3X
    You got a
    2nd GOAL

    View Slide

  64. 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.

    View Slide

  65. • 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

    View Slide

  66. 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.

    View Slide

  67. 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

    View Slide

  68. 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.

    View Slide

  69. 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.

    View Slide

  70. think
    code
    run
    TDD supports both think and code.
    Programming workflow

    View Slide

  71. 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...

    View Slide

  72. 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.

    View Slide

  73. think
    code
    run
    Programming workflow

    View Slide

  74. think
    code
    run
    The more difficult a code you write,
    the helpful this approach can be.
    Programming workflow

    View Slide

  75. View Slide

  76. 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.

    View Slide

  77. Questions?

    View Slide

  78. FAQ. How about cuke,
    T/U, shoulda or ... ?
    • It's not matter.
    • code > run > think loop should be test
    framework agnostic .

    View Slide