How To Write Tests & Stories For Ruby Gems -- Jaoo 2009

3,363 views

Published on

You can write a small Ruby library in only a few lines. But then it grows, it expands and then it starts to break and become a maintenance nightmare. Since its open source you just stop working on it. Users complain that the project has been abandoned. Your project ends up causing more grief for everyone than if you'd never written it at all.

Instead, we will learn to write all Ruby libraries, RubyGems with tests.

This session is NOT about "how to do TDD". More importantly this session will teach you:

* the one command you should run before starting any new Ruby project

* the best way to write tests for command-line apps, Rake tasks and other difficult to test code

* how to do Continuous Integration of your Ruby/Rails libraries with runcoderun.com

Once you know how to write tests for all Ruby code, you'll want to do it for even the smallest little libraries and have the confidence to know your code always works.

Published in: Technology

How To Write Tests & Stories For Ruby Gems -- Jaoo 2009

  1. 1. How to write tests/ stories for RubyGems Dr Nic Williams mocra.com drnicwilliams.com @drnic $ sudo gem install tweettail $ tweettail jaoo -f
  2. 2. JAOO.au loves Ruby
  3. 3. http://www.slideshare.com/drnic
  4. 4. tweettail $ sudo gem install tweettail $ tweettail jaoo mattnhodges: Come speak with me at JAOO next week - http://jaoo.dk/ Steve_Hayes: @VenessaP I think they went out for noodles. #jaoo theRMK: Come speak with Matt at JAOO next week drnic: reading my own abstract for JAOO presentation
  5. 5. New Gem in 2min newgem tweet-tail cd tweet-tail script/generate executable tweettail rake manifest rake install_gem
  6. 6. Rakefile $hoe = Hoe.new('tweettail', TweetTail::VERSION) do |p| p.developer('FIXME full name', 'FIXME email') ... end $hoe = Hoe.new('tweettail', TweetTail::VERSION) do |p| p.developer('Dr Nic', 'drnicwilliams@gmail.com') ... end
  7. 7. README.rdoc = tweet-tail * FIX (url) = tweet-tail * http://github.com/drnic/tweet-tail
  8. 8. New Gem in 2min cont... newgem tweet-tail cd tweet-tail script/generate executable tweettail rake manifest rake install_gem tweettail SUCCESS!! To update this executable, look in lib/tweet-tail/cli.rb
  9. 9. User story Feature: Live twitter search results on the command line In order to reduce cost of getting live search results As a twitter user I want twitter search results appearing in the console
  10. 10. Describe behaviour in plain text Write a step definition in Ruby Run and watch it fail Fix code Run and watch it pass!
  11. 11. Install cucumber sudo gem install cucumber script/generate install_cucumber cp story_text features/command_line_app.feature
  12. 12. features/cli.feature Feature: Live twitter search results on command line In order to reduce cost of getting live search results As a twitter user I want twitter search results appearing in the console Scenario: Display current search results Given twitter has some search results for 'jaoo' When I run local executable 'tweettail' with arguments 'jaoo' Then I should see quot;quot;quot; mattnhodges: Come speak with me at JAOO next week... Steve_Hayes: @VenessaP I think they went out for... theRMK: Come speak with Matt at JAOO next week... drnic: reading my own abstract for JAOO presentation... quot;quot;quot;
  13. 13. Running scenario $ cucumber features/command_line_app.feature ... 1 scenario 2 skipped steps 1 undefined step You can implement step definitions for missing steps with these snippets: Given /^twitter has some search results for quot;([^quot;]*)quot;$/ do |arg1| pending end
  14. 14. features/step_definitions/ twitter_data_steps.rb Given /^twitter has some search results for quot;([^quot;]*)quot;$/ do |query| FakeWeb.register_uri( :get, quot;http://search.twitter.com/search.json?q=#{query}quot;, :file => File.dirname(__FILE__) + quot;/../fixtures/search-#{query}.jsonquot;) end
  15. 15. features/step_definitions/ twitter_data_steps.rb mkdir -p features/fixtures curl http://search.twitter.com/search.json?q=jaoo > features/fixtures/search-jaoo.json
  16. 16. features/fixtures/search-jaoo.rb { quot;resultsquot;: [ { quot;textquot;: quot;reading my own abstract for JAOO presentationquot;, quot;from_userquot;: quot;drnicquot;, quot;idquot;: 1666627310 }, { quot;textquot;: quot;Come speak with Matt at JAOO next weekquot;, quot;from_userquot;: quot;theRMKquot;, quot;idquot;: 1666334207 }, { quot;textquot;: quot;@VenessaP I think they went out for noodles. #jaooquot;, quot;from_userquot;: quot;Steve_Hayesquot;, quot;idquot;: 1666166639 }, { quot;textquot;: quot;Come speak with me at JAOO next week - http://jaoo.dk/quot;, quot;from_userquot;: quot;mattnhodgesquot;, quot;idquot;: 1664823944, }], quot;refresh_urlquot;: quot;?since_id=1682666650&q=jaooquot;, quot;results_per_pagequot;: 15, quot;next_pagequot;: quot;?page=2&max_id=1682666650&q=jaooquot;
  17. 17. features/common/env.rb gem quot;fakewebquot; require quot;fakewebquot; Before do FakeWeb.allow_net_connect = false end
  18. 18. Running scenario $ cucumber features/command_line_app.feature ... Scenario: Display current search results Given a safe folder And twitter has some search results for quot;jaooquot; When I run local executable quot;tweettailquot; with arguments quot;jaooquot; Then I should see quot;quot;quot; mattnhodges: Come speak with me at JAOO next week... Steve_Hayes: @VenessaP I think they went out for noodles... theRMK: Come speak with Matt at JAOO next week drnic: reading my own abstract for JAOO presentation quot;quot;quot; 1 scenario 1 failed step 3 passed steps
  19. 19. http://www.slideshare.net/bmabey/outsidein-development-with-cucumber
  20. 20. fetching JSON feed def initial_json_data Net::HTTP.get(URI.parse(quot;http://search.twitter.com/search.json?q=#{query}quot;)) end
  21. 21. fakeweb failure?! Scenario: Display current search results Given a safe folder And twitter has some search results for quot;jaooquot; When I run local executable quot;tweettailquot; with arguments quot;jaooquot; getaddrinfo: nodename nor servname provided, or not known (SocketError) from .../net/http.rb:564:in `open' ... from .../tweet-tail/lib/tweet-tail/tweet_poller.rb:24:in `initial_json_data' from .../tweet-tail/lib/tweet-tail/tweet_poller.rb:9:in `refresh' from .../tweet-tail/lib/tweet-tail/cli.rb:39:in `execute' from .../tweet-tail/bin/tweet-tail:10 Then I dump stdout
  22. 22. features/step_definitions/ common_steps.rb When /^I run local executable quot;(.*)quot; with arguments quot;(.*)quot;/ do |exec, arguments| @stdout = File.expand_path(File.join(@tmp_root, quot;executable.outquot;)) executable = File.expand_path(File.join(File.dirname(__FILE__), quot;/../../binquot;, exec)) in_project_folder do system quot;ruby #{executable} #{arguments} > #{@stdout}quot; end end
  23. 23. Can I ignore a There’s probably always something you can’t quite test Minimise that layer of code Test the rest bin main lib
  24. 24. Can I ignore a There’s probably always something you can’t quite test Minimise that layer of code Test the rest bin main lib 1x sanity check
  25. 25. Can I ignore a There’s probably always something you can’t quite test Minimise that layer of code Test the rest bin main lib 1x sanity check all other integration tests on internal code
  26. 26. bin/tweettail ed to test this? Do I ne #!/usr/bin/env ruby # # Created on 2009-5-1 by Dr Nic Williams # Copyright (c) 2009. All rights reserved. require 'rubygems' require File.expand_path(File.dirname(__FILE__) + quot;/../lib/tweet-tailquot;) require quot;tweet-tail/cliquot; TweetTail::CLI.execute(STDOUT, ARGV)
  27. 27. features/cli.feature ... Scenario: Display current search results Given twitter has some search results for 'jaoo' When I run local executable 'tweettail' with arguments 'jaoo' Then I should see quot;quot;quot; mattnhodges: Come speak with me at JAOO next week... Steve_Hayes: @VenessaP I think they went out for... theRMK: Come speak with Matt at JAOO next week... drnic: reading my own abstract for JAOO presentation... quot;quot;quot;
  28. 28. features/cli.feature ... Scenario: Display some search results Given a safe folder And twitter has some search results for quot;jaooquot; When I run local executable quot;tweettailquot; with arguments quot;jaooquot; Then I should see some twitter messages Scenario: Display explicit search results Given a safe folder And twitter has some search results for quot;jaooquot; When I run executable internally with arguments quot;jaooquot; Then I should see quot;quot;quot; mattnhodges: Come speak with me at JAOO next week Steve_Hayes: @VenessaP I think they went out for theRMK: Come speak with Matt at JAOO next week drnic: reading my own abstract for JAOO presentation quot;quot;quot;
  29. 29. end result $ rake install_gem $ tweettail jaoo JAOO: Linda R.: I used to be a mathematician - I couldn't very well have started... bengeorge: Global Financial Crisises are cool: jaoo tix down to 250 for 2 days. kflund: First day of work at the JAOO Tutorials in Sydney - visiting the Opera House wa7son: To my Copenhagen Ruby or Java colleagues: Get to meet Ola Bini at JAOO Geek Nights ldaley: I am going to JAOO... awesome. jessechilcott: @smallkathryn it's an IT conference. http://jaoo.com.au/sydney-2009/ . scotartt: Looking forward to JAOO Brisbane next week - http://jaoo.com.au/brisbane-2009/ scotartt: JAOO Brisbane 2009 http://ff.im/-2B5ja gwillis: @tweval I would give #jaoo a 10.0 rowanb: Bags almost packed for Sydney. Scrum User Group then JAOO. Driving there mattnhodges: busy rest of week ahead. Spking @ Wiki Wed. Atlassian booth babe @ JAOO conference Syd Thurs & Fri. Kiama 4 Jase's wedding all w'end #fb pcalcado: searching twiter for #jaoo first impressions. kornys: #jaoo has been excellent so far - though my tutorials have been full of Steve_Hayes: RT @martinjandrews: women in rails - provide child care at #railsconf CaioProiete: Wish I could be at #JAOO Australia...
  30. 30. ‘I run executable internally’ step defn When /^I run executable internally with arguments quot;(.*)quot;/ do |arguments| require 'rubygems' require File.dirname(__FILE__) + quot;/../../lib/tweet-tailquot; require quot;tweet-tail/cliquot; @stdout = File.expand_path(File.join(@tmp_root, quot;executable.outquot;)) in_project_folder do TweetTail::CLI.execute(@stdout_io = StringIO.new, arguments.split(quot; quot;)) @stdout_io.rewind File.open(@stdout, quot;wquot;) { |f| f << @stdout_io.read } end end
  31. 31. Many provided steps Given /^a safe folder/ do Given /^this project is active project folder/ do Given /^env variable $([w_]+) set to quot;(.*)quot;/ do |env_var, value| Given /quot;(.*)quot; folder is deleted/ do |folder| When /^I invoke quot;(.*)quot; generator with arguments quot;(.*)quot;$/ do |generator, args| When /^I run executable quot;(.*)quot; with arguments quot;(.*)quot;/ do |executable, args| When /^I run project executable quot;(.*)quot; with arguments quot;(.*)quot;/ do |executable, args| When /^I run local executable quot;(.*)quot; with arguments quot;(.*)quot;/ do |executable, args| When /^I invoke task quot;rake (.*)quot;/ do |task| Then /^folder quot;(.*)quot; (is|is not) created/ do |folder, is| Then /^file quot;(.*)quot; (is|is not) created/ do |file, is| Then /^file with name matching quot;(.*)quot; is created/ do |pattern| Then /^file quot;(.*)quot; contents (does|does not) match /(.*)// do |file, does, regex| Then /^(does|does not) invoke generator quot;(.*)quot;$/ do |does_invoke, generator| Then /^I should see$/ do |text| Then /^I should not see$/ do |text| Then /^I should see exactly$/ do |text| Then /^I should see all (d+) tests pass/ do |expected_test_count| Then /^I should see all (d+) examples pass/ do |expected_test_count| Then /^Rakefile can display tasks successfully/ do Then /^task quot;rake (.*)quot; is executed successfully/ do |task|
  32. 32. ‘I should see...’ features/step_definitions/common_steps.rb Then /^I should see$/ do |text| actual_output = File.read(@stdout) actual_output.should contain(text) end Then /^I should not see$/ do |text| actual_output = File.read(@stdout) actual_output.should_not contain(text) end Then /^I should see exactly$/ do |text| actual_output = File.read(@stdout) actual_output.should == text end
  33. 33. ‘When I do something...’ features/step_definitions/common_steps.rb When /^I run project executable quot;(.*)quot; with arguments quot;(.*)quot;/ do |executable, args| @stdout = File.expand_path(File.join(@tmp_root, quot;executable.outquot;)) in_project_folder do system quot;ruby #{executable} #{arguments} > #{@stdout}quot; end end When /^I invoke task quot;rake (.*)quot;/ do |task| @stdout = File.expand_path(File.join(@tmp_root, quot;tests.outquot;)) in_project_folder do system quot;rake #{task} --trace > #{@stdout}quot; end end
  34. 34. ‘Given a safe folder...’ features/support/env.rb Before do @tmp_root = File.dirname(__FILE__) + quot;/../../tmpquot; @home_path = File.expand_path(File.join(@tmp_root, quot;homequot;)) FileUtils.rm_rf @tmp_root FileUtils.mkdir_p @home_path ENV[quot;HOMEquot;] = @home_path end
  35. 35. tweettail jaoo -f polling please? how to test polling?
  36. 36. features/cli.feature Scenario: Poll for results until app cancelled Given twitter has some search results for quot;jaooquot; When I run executable internally with arguments quot;jaoo -fquot; Then I should see quot;quot;quot; mattnhodges: Come speak with me at JAOO next week Steve_Hayes: @VenessaP I think they went out for theRMK: Come speak with Matt at JAOO next week drnic: reading my own abstract for JAOO presentation quot;quot;quot; When the sleep period has elapsed Then I should see quot;quot;quot; mattnhodges: Come speak with me at JAOO next week Steve_Hayes: @VenessaP I think they went out for theRMK: Come speak with Matt at JAOO next week drnic: reading my own abstract for JAOO presentation CaioProiete: Wish I could be at #JAOO Australia... quot;quot;quot; When I press quot;Ctrl-Cquot; ...
  37. 37. adding -f option $ cucumber features/cli.feature:22 ... Scenario: Poll for results until app cancelled Given twitter has some search results for quot;jaooquot; When I run executable internally with arguments quot;jaoo -fquot; invalid option: -f (OptionParser::InvalidOption) lib/tweet-tail/cli.rb module TweetTail::CLI def self.execute(stdout, arguments=[]) options = { :polling => false } parser = OptionParser.new do |opts| opts.on(quot;-fquot;, quot;Poll for new search results each 15 seconds.quot; ) { |arg| options[:polling] = true } opts.parse!(arguments) end app = TweetTail::TweetPoller.new(arguments.shift, options) app.refresh stdout.puts app.render_latest_results end end
  38. 38. features/fixtures/ search-jaoo- { quot;resultsquot;: [{ quot;textquot;: quot;Wish I could be at #JAOO Australia...quot;, quot;from_userquot;: quot;CaioProietequot;, quot;idquot;: 1711269079, }], quot;since_idquot;: 1682666650, quot;refresh_urlquot;: quot;?since_id=1711269079&q=jaooquot;, quot;queryquot;: quot;jaooquot; }
  39. 39. features/step_definitions/ twitter_data_steps.rb Given /^twitter has some search results for quot;([^quot;]*)quot;$/ do |query| FakeWeb.register_uri( :get, quot;http://search.twitter.com/search.json?q=#{query}quot;, :file => File.expand_path(File.dirname(__FILE__) + quot;/../fixtures/search-#{query}.jsonquot;)) since = quot;1682666650quot; FakeWeb.register_uri( :get, quot;http://search.twitter.com/search.json?since_id=#{since}&q=#{query}quot;, :file => File.expand_path(File.dirname(__FILE__) + quot;/../fixtures/search-#{query}-since-#{since}.jsonquot;)) end
  40. 40. hmm, sleep... $ cucumber features/cli.feature:22 ... Scenario: Poll for results until app cancelled Given twitter has some search results for quot;jaooquot; When I run executable internally with arguments quot;jaoo -fquot; Then I should see quot;quot;quot; mattnhodges: Come speak with me at JAOO next week - http://jaoo.dk/ Steve_Hayes: @VenessaP I think they went out for noodles. #jaoo theRMK: Come speak with Matt at JAOO next week drnic: reading my own abstract for JAOO presentation quot;quot;quot; When the sleep period has elapsed Then I should see quot;quot;quot; mattnhodges: Come speak with me at JAOO next week - http://jaoo.dk/ Steve_Hayes: @VenessaP I think they went out for noodles. #jaoo theRMK: Come speak with Matt at JAOO next week drnic: reading my own abstract for JAOO presentation CaioProiete: Wish I could be at #JAOO Australia... quot;quot;quot;
  41. 41. features/cli.feature Scenario: Poll for results until app cancelled Given twitter has some search results for quot;jaooquot; When I run executable internally with arguments quot;jaoo -fquot; and wait 1 sleep cycle and quit Then I should see quot;quot;quot; mattnhodges: Come speak with me at JAOO next week Steve_Hayes: @VenessaP I think they went out for... theRMK: Come speak with Matt at JAOO next week drnic: reading my own abstract for JAOO presentation CaioProiete: Wish I could be at #JAOO Australia... quot;quot;quot;
  42. 42. features/step_definitions/executable_steps.rb When /^I run executable internally with arguments quot;([^quot;]*)quot; and wait (d+) sleep cycles? and quit$/ do |args, cycles| hijack_sleep(cycles.to_i) When %Q{I run executable internally with arguments quot;#{args}quot;} end features/support/time_machine_helpers.rb module TimeMachineHelper # expects sleep() to be called +cycles+ times, and then raises an Interrupt def hijack_sleep(cycles) results = [*1..cycles] # irrelevant truthy values for each sleep call Kernel::stubs(:sleep).returns(*results).then.raises(Interrupt) end end World(TimeMachineHelper)
  43. 43. using mocha require quot;mochaquot; World(Mocha::Standalone) Before do mocha_setup end After do begin mocha_verify ensure mocha_teardown end end features/support/mocha.rb
  44. 44. working! $ cucumber features/cli.feature:22 Feature: Live twitter search results on command line In order to reduce cost of getting live search results As a twitter user I want twitter search results appearing in the console Scenario: Poll for results until app cancelled Given twitter has some search results for quot;jaooquot; When I run executable internally with arguments quot;jaoo -fquot; and wait 1 sleep cycle and quit Then I should see quot;quot;quot; mattnhodges: Come speak with me at JAOO next week - http://jaoo.dk/ Steve_Hayes: @VenessaP I think they went out for noodles. #jaoo theRMK: Come speak with Matt at JAOO next week drnic: reading my own abstract for JAOO presentation CaioProiete: Wish I could be at #JAOO Australia... quot;quot;quot; 1 scenario (1 passed) 3 steps (3 passed)
  45. 45. Rakefile task :default => [:features] $ rake (in /Users/drnic/Documents/ruby/gems/tweet-tail) /System/Library/Frameworks/Ruby.framework/Versions/1.8/usr/bin/ruby -w - Ilib:ext:bin:test -e 'require quot;rubygemsquot;; require quot;test/unitquot;; require quot;test/ test_helper.rbquot;; require quot;test/test_tweet_poller.rbquot;' Started .... Finished in 0.002231 seconds. 4 tests, 10 assertions, 0 failures, 0 errors /System/Library/Frameworks/Ruby.framework/Versions/1.8/usr/bin/ruby -I quot;/Library/ Ruby/Gems/1.8/gems/cucumber-0.3.2/lib:libquot; ... ................. 5 scenarios (5 passed) 17 steps (17 passed) Run unit tests + features
  46. 46. 1. Create account with http://runcoderun.com 2. Press ‘Test Hook’
  47. 47. 1. Create account with http://runcoderun.com 2. Press ‘Test Hook’
  48. 48. How to write tests/ stories for RubyGems Dr Nic Williams mocra.com drnicwilliams.com twitter: @drnic drnic@mocra.com

×