Slide 1

Slide 1 text

Let your test drive your development Kyosuke MOROHASHI

Slide 2

Slide 2 text

No content

Slide 3

Slide 3 text

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

Slide 4

Slide 4 text

No content

Slide 5

Slide 5 text

No content

Slide 6

Slide 6 text

No content

Slide 7

Slide 7 text

=begin PR about us

Slide 8

Slide 8 text

No content

Slide 9

Slide 9 text

No content

Slide 10

Slide 10 text

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

Slide 11

Slide 11 text

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

Slide 12

Slide 12 text

You can paste both text snippet and picture.

Slide 13

Slide 13 text

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

Slide 14

Slide 14 text

No content

Slide 15

Slide 15 text

=end

Slide 16

Slide 16 text

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

Slide 17

Slide 17 text

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

Slide 18

Slide 18 text

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?

Slide 19

Slide 19 text

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

Slide 20

Slide 20 text

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

Slide 21

Slide 21 text

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

Slide 22

Slide 22 text

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.

Slide 23

Slide 23 text

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

Slide 24

Slide 24 text

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

Slide 25

Slide 25 text

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

Slide 26

Slide 26 text

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

Slide 27

Slide 27 text

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.

Slide 28

Slide 28 text

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.

Slide 29

Slide 29 text

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.

Slide 30

Slide 30 text

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

Slide 31

Slide 31 text

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.

Slide 32

Slide 32 text

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

Slide 33

Slide 33 text

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.

Slide 34

Slide 34 text

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.

Slide 35

Slide 35 text

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.

Slide 36

Slide 36 text

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.

Slide 37

Slide 37 text

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

Slide 38

Slide 38 text

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

Slide 39

Slide 39 text

An ordinary search query form with ordinary tables.

Slide 40

Slide 40 text

•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

Slide 41

Slide 41 text

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

Slide 42

Slide 42 text

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

Slide 43

Slide 43 text

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?

Slide 44

Slide 44 text

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

Slide 45

Slide 45 text

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

Slide 46

Slide 46 text

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

Slide 47

Slide 47 text

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

Slide 48

Slide 48 text

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.

Slide 49

Slide 49 text

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

Slide 50

Slide 50 text

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.

Slide 51

Slide 51 text

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

Slide 52

Slide 52 text

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

Slide 53

Slide 53 text

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

Slide 54

Slide 54 text

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

Slide 55

Slide 55 text

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

Slide 56

Slide 56 text

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.

Slide 57

Slide 57 text

No content

Slide 58

Slide 58 text

• 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

Slide 59

Slide 59 text

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

Slide 60

Slide 60 text

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

Slide 61

Slide 61 text

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

Slide 62

Slide 62 text

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.

Slide 63

Slide 63 text

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

Slide 64

Slide 64 text

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.

Slide 65

Slide 65 text

• 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

Slide 66

Slide 66 text

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.

Slide 67

Slide 67 text

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

Slide 68

Slide 68 text

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.

Slide 69

Slide 69 text

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.

Slide 70

Slide 70 text

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

Slide 71

Slide 71 text

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

Slide 72

Slide 72 text

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.

Slide 73

Slide 73 text

think code run Programming workflow

Slide 74

Slide 74 text

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

Slide 75

Slide 75 text

No content

Slide 76

Slide 76 text

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.

Slide 77

Slide 77 text

Questions?

Slide 78

Slide 78 text

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