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?
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.
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.
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.
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
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.
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.
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.
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.
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.
Test drives development by: Structuring execution •Testing framework structure your code execution. •Arrange a context •Act / perform inependentry •Assert result
•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
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?
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
• 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?
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.
• 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?
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.
• 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.
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
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.
• 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
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.
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.
• 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
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.
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.
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.
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...
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.
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.