64
How to write tests/ stories for RubyGems Dr Nic Williams mocra.com drnicwilliams.com @drnic $ sudo gem install tweettail $ tweettail jaoo -f 1 Thursday, 7 May 2009 Abstract: 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 dicult 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.

How to write tests/ stories for RubyGemsgotocon.com/dl/jaoo-sydney-2009/slides/NicWilliams_HowToDoWrite… · Since we’re at JAOO, we want to track conversations on twitter about

  • Upload
    others

  • View
    0

  • Download
    0

Embed Size (px)

Citation preview

Page 1: How to write tests/ stories for RubyGemsgotocon.com/dl/jaoo-sydney-2009/slides/NicWilliams_HowToDoWrite… · Since we’re at JAOO, we want to track conversations on twitter about

How to write tests/stories for RubyGems

Dr Nic Williamsmocra.com

drnicwilliams.com@drnic

$ sudo gem install tweettail$ tweettail jaoo -f

1Thursday, 7 May 2009

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

Page 2: How to write tests/ stories for RubyGemsgotocon.com/dl/jaoo-sydney-2009/slides/NicWilliams_HowToDoWrite… · Since we’re at JAOO, we want to track conversations on twitter about

JAOO.au loves Ruby

2Thursday, 7 May 2009

JAOO.au 2008 had no Ruby talks. It had an F# talk but no Ruby talks. There were Ruby groups all around Australia. Perhaps there are secret F# clubs I don’t know about.

But this year we have a whole afternoon of Ruby mixed in with the rest of the awesomeness within this conference.

Page 3: How to write tests/ stories for RubyGemsgotocon.com/dl/jaoo-sydney-2009/slides/NicWilliams_HowToDoWrite… · Since we’re at JAOO, we want to track conversations on twitter about

http://www.slideshare.com/drnic

3Thursday, 7 May 2009

Page 4: How to write tests/ stories for RubyGemsgotocon.com/dl/jaoo-sydney-2009/slides/NicWilliams_HowToDoWrite… · Since we’re at JAOO, we want to track conversations on twitter about

4Thursday, 7 May 2009

Since we’re at JAOO, we want to track conversations on twitter about JAOO.

Since we’re nerds, let’s pull these into to the command line and print them out.

Perhaps we’ll also add a “tail” feature to sit there watching for new tweets as they come along.

A twitter terminal client. Very nerdy. Very JAOO.

Page 5: How to write tests/ stories for RubyGemsgotocon.com/dl/jaoo-sydney-2009/slides/NicWilliams_HowToDoWrite… · Since we’re at JAOO, we want to track conversations on twitter about

tweettail

$ sudo gem install tweettail$ tweettail jaoomattnhodges: Come speak with me at JAOO next week - http://jaoo.dk/Steve_Hayes: @VenessaP I think they went out for noodles. #jaootheRMK: Come speak with Matt at JAOO next weekdrnic: reading my own abstract for JAOO presentation

5Thursday, 7 May 2009

This would be our sample (truncated) output from the previous example page.

It will then sit there pulling down search results and printing new search results when they appear.

We’ll use Ctrl-C to cancel.

Page 6: How to write tests/ stories for RubyGemsgotocon.com/dl/jaoo-sydney-2009/slides/NicWilliams_HowToDoWrite… · Since we’re at JAOO, we want to track conversations on twitter about

New Gem in 2min

newgem tweet-tailcd tweet-tailscript/generate executable tweettailrake manifestrake install_gem

6Thursday, 7 May 2009

‘newgem’ is the one command you should run before starting any new Ruby project or Rails plugin.

Page 7: How to write tests/ stories for RubyGemsgotocon.com/dl/jaoo-sydney-2009/slides/NicWilliams_HowToDoWrite… · Since we’re at JAOO, we want to track conversations on twitter about

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', '[email protected]') ...end

7Thursday, 7 May 2009

Page 8: How to write tests/ stories for RubyGemsgotocon.com/dl/jaoo-sydney-2009/slides/NicWilliams_HowToDoWrite… · Since we’re at JAOO, we want to track conversations on twitter about

= tweet-tail

* http://github.com/drnic/tweet-tail

README.rdoc= tweet-tail

* FIX (url)

8Thursday, 7 May 2009

Page 9: How to write tests/ stories for RubyGemsgotocon.com/dl/jaoo-sydney-2009/slides/NicWilliams_HowToDoWrite… · Since we’re at JAOO, we want to track conversations on twitter about

New Gem in 2min

newgem tweet-tailcd tweet-tailscript/generate executable tweettailrake manifestrake install_gemtweettail SUCCESS!! To update this executable, look in lib/tweet-tail/cli.rb

cont...

9Thursday, 7 May 2009

And we have a working command line application!

Page 10: How to write tests/ stories for RubyGemsgotocon.com/dl/jaoo-sydney-2009/slides/NicWilliams_HowToDoWrite… · Since we’re at JAOO, we want to track conversations on twitter about

User storyFeature: 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

10Thursday, 7 May 2009

In agile development we describe a user story. What is it that the user wants from our software system? And what is the value to them for it.

Page 11: How to write tests/ stories for RubyGemsgotocon.com/dl/jaoo-sydney-2009/slides/NicWilliams_HowToDoWrite… · Since we’re at JAOO, we want to track conversations on twitter about

Describe behaviour in plain text

Write a step definition in Ruby

Run and watch it fail

Fix code

Run and watch it pass!

11Thursday, 7 May 2009

Page 12: How to write tests/ stories for RubyGemsgotocon.com/dl/jaoo-sydney-2009/slides/NicWilliams_HowToDoWrite… · Since we’re at JAOO, we want to track conversations on twitter about

Install cucumber

sudo gem install cucumberscript/generate install_cucumbercp story_text features/command_line_app.feature

12Thursday, 7 May 2009

story_text is the content from the “Basic user story” slide

Page 13: How to write tests/ stories for RubyGemsgotocon.com/dl/jaoo-sydney-2009/slides/NicWilliams_HowToDoWrite… · Since we’re at JAOO, we want to track conversations on twitter about

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

features/cli.feature

13Thursday, 7 May 2009

Page 14: How to write tests/ stories for RubyGemsgotocon.com/dl/jaoo-sydney-2009/slides/NicWilliams_HowToDoWrite… · Since we’re at JAOO, we want to track conversations on twitter about

Running scenario$ cucumber features/command_line_app.feature...1 scenario2 skipped steps1 undefined step

You can implement step definitions for missing stepswith these snippets:

Given /^twitter has some search results for "([^\"]*)"$/ do |arg1| pendingend

14Thursday, 7 May 2009

Page 15: How to write tests/ stories for RubyGemsgotocon.com/dl/jaoo-sydney-2009/slides/NicWilliams_HowToDoWrite… · Since we’re at JAOO, we want to track conversations on twitter about

features/step_definitions/twitter_data_steps.rb

Given /^twitter has some search results for "([^\"]*)"$/ do |query| FakeWeb.register_uri( :get, "http://search.twitter.com/search.json?q=#{query}", :file => File.dirname(__FILE__) + "/../fixtures/search-#{query}.json")end

15Thursday, 7 May 2009

If this search query is called then the contents of the fixtures fill will be returned.

Page 16: How to write tests/ stories for RubyGemsgotocon.com/dl/jaoo-sydney-2009/slides/NicWilliams_HowToDoWrite… · Since we’re at JAOO, we want to track conversations on twitter about

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

16Thursday, 7 May 2009

If this search query is called then the contents of the fixtures fill will be returned.

Page 17: How to write tests/ stories for RubyGemsgotocon.com/dl/jaoo-sydney-2009/slides/NicWilliams_HowToDoWrite… · Since we’re at JAOO, we want to track conversations on twitter about

{ "results": [ { "text": "reading my own abstract for JAOO presentation", "from_user": "drnic", "id": 1666627310 }, { "text": "Come speak with Matt at JAOO next week", "from_user": "theRMK", "id": 1666334207 }, { "text": "@VenessaP I think they went out for noodles. #jaoo", "from_user": "Steve_Hayes", "id": 1666166639 }, { "text": "Come speak with me at JAOO next week - http:\/\/jaoo.dk\/", "from_user": "mattnhodges", "id": 1664823944, }], "refresh_url": "?since_id=1682666650&q=jaoo", "results_per_page": 15, "next_page": "?page=2&max_id=1682666650&q=jaoo"}

features/fixtures/search-jaoo.rb

17Thursday, 7 May 2009

Save a real copy of actual data from the target feed into your project.

Page 18: How to write tests/ stories for RubyGemsgotocon.com/dl/jaoo-sydney-2009/slides/NicWilliams_HowToDoWrite… · Since we’re at JAOO, we want to track conversations on twitter about

features/common/env.rb

gem "fakeweb"require "fakeweb"

Before do FakeWeb.allow_net_connect = falseend

18Thursday, 7 May 2009

This wires fakeweb into cucumber and asks it to throw errors if we ever ask for remote content that hasn’t been setup via FakeWeb.request_uri

Page 19: How to write tests/ stories for RubyGemsgotocon.com/dl/jaoo-sydney-2009/slides/NicWilliams_HowToDoWrite… · Since we’re at JAOO, we want to track conversations on twitter about

Running scenario$ cucumber features/command_line_app.feature... Scenario: Display current search results Given a safe folder And twitter has some search results for "jaoo" When I run local executable "tweettail" with arguments "jaoo" Then I should see """ 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 """

1 scenario1 failed step3 passed steps

19Thursday, 7 May 2009

So when we run our feature scenarios again we just get the error.

Why? We haven’t written any code yet; but we’ve finished setting up our integration test.

Page 20: How to write tests/ stories for RubyGemsgotocon.com/dl/jaoo-sydney-2009/slides/NicWilliams_HowToDoWrite… · Since we’re at JAOO, we want to track conversations on twitter about

http://www.slideshare.net/bmabey/outsidein-development-with-cucumber20Thursday, 7 May 2009

Ben Mabey took 137 slides to discuss when and why to start with Cucumber and then progress to unit tests of sections of your code.

Page 21: How to write tests/ stories for RubyGemsgotocon.com/dl/jaoo-sydney-2009/slides/NicWilliams_HowToDoWrite… · Since we’re at JAOO, we want to track conversations on twitter about

fetching JSON feed

def initial_json_data Net::HTTP.get(URI.parse("http://search.twitter.com/search.json?q=#{query}"))end

21Thursday, 7 May 2009

Somewhere in our solution code we call out to the json feed, parse the JSON, and print out the results.

Page 22: How to write tests/ stories for RubyGemsgotocon.com/dl/jaoo-sydney-2009/slides/NicWilliams_HowToDoWrite… · Since we’re at JAOO, we want to track conversations on twitter about

fakeweb failure?! Scenario: Display current search results Given a safe folder And twitter has some search results for "jaoo" When I run local executable "tweettail" with arguments "jaoo" 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

22Thursday, 7 May 2009

If I disable my internet and run my tests then I should definitely see fakeweb coming into effect. But it doesn’t. Bugger.

This is because “When I run local executable...” invokes the executable in new Ruby process. It knows nothing about fakeweb.

Page 23: How to write tests/ stories for RubyGemsgotocon.com/dl/jaoo-sydney-2009/slides/NicWilliams_HowToDoWrite… · Since we’re at JAOO, we want to track conversations on twitter about

features/step_definitions/common_steps.rb

When /^I run local executable "(.*)" with arguments "(.*)"/ do |exec, arguments| @stdout = File.expand_path(File.join(@tmp_root, "executable.out")) executable = File.expand_path(File.join(File.dirname(__FILE__), "/../../bin", exec)) in_project_folder do system "ruby #{executable} #{arguments} > #{@stdout}" endend

23Thursday, 7 May 2009

This is the default implementation of “run local executable”. We actually make a pure external system call to run the app, just like a user.

Page 24: How to write tests/ stories for RubyGemsgotocon.com/dl/jaoo-sydney-2009/slides/NicWilliams_HowToDoWrite… · Since we’re at JAOO, we want to track conversations on twitter about

Can I ignore a layer?There’s probably always something you can’t quite test

Minimise that layer of code

Test the rest

main libbin

24Thursday, 7 May 2009

If you do need to stub out something - a remote service, change the clock, speed things up, then its a lot easier to do it within the same Ruby process.

So we’ll break up our integration test: 1 sanity check to test that the bin/tweettail executable is wired up correctly and pulls down some twitter data.

The rest of our integration tests will invoke the library code directly within the same Ruby process.

Page 25: How to write tests/ stories for RubyGemsgotocon.com/dl/jaoo-sydney-2009/slides/NicWilliams_HowToDoWrite… · Since we’re at JAOO, we want to track conversations on twitter about

Can I ignore a layer?There’s probably always something you can’t quite test

Minimise that layer of code

Test the rest

main libbin

1x sanity check

24Thursday, 7 May 2009

If you do need to stub out something - a remote service, change the clock, speed things up, then its a lot easier to do it within the same Ruby process.

So we’ll break up our integration test: 1 sanity check to test that the bin/tweettail executable is wired up correctly and pulls down some twitter data.

The rest of our integration tests will invoke the library code directly within the same Ruby process.

Page 26: How to write tests/ stories for RubyGemsgotocon.com/dl/jaoo-sydney-2009/slides/NicWilliams_HowToDoWrite… · Since we’re at JAOO, we want to track conversations on twitter about

Can I ignore a layer?There’s probably always something you can’t quite test

Minimise that layer of code

Test the rest

main libbin

1x sanity check all other integration testson internal code

24Thursday, 7 May 2009

If you do need to stub out something - a remote service, change the clock, speed things up, then its a lot easier to do it within the same Ruby process.

So we’ll break up our integration test: 1 sanity check to test that the bin/tweettail executable is wired up correctly and pulls down some twitter data.

The rest of our integration tests will invoke the library code directly within the same Ruby process.

Page 27: How to write tests/ stories for RubyGemsgotocon.com/dl/jaoo-sydney-2009/slides/NicWilliams_HowToDoWrite… · Since we’re at JAOO, we want to track conversations on twitter about

#!/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__) + "/../lib/tweet-tail")require "tweet-tail/cli"

TweetTail::CLI.execute(STDOUT, ARGV)

bin/tweettail

Do I need to test this?

25Thursday, 7 May 2009

There is a very thin wrapper around the library code which was auto-generated. This helps you trust that it should “Just Work”. So let’s just test the final part instead to test the ultimate result

Page 28: How to write tests/ stories for RubyGemsgotocon.com/dl/jaoo-sydney-2009/slides/NicWilliams_HowToDoWrite… · Since we’re at JAOO, we want to track conversations on twitter about

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

features/cli.feature

26Thursday, 7 May 2009

Original version that we couldn’t fake out the remote data...

Page 29: How to write tests/ stories for RubyGemsgotocon.com/dl/jaoo-sydney-2009/slides/NicWilliams_HowToDoWrite… · Since we’re at JAOO, we want to track conversations on twitter about

... Scenario: Display some search results Given a safe folder And twitter has some search results for "jaoo" When I run local executable "tweettail" with arguments "jaoo" Then I should see some twitter messages

Scenario: Display explicit search results Given a safe folder And twitter has some search results for "jaoo" When I run executable internally with arguments "jaoo" Then I should see """ 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 """

features/cli.feature

27Thursday, 7 May 2009

New version where we can.

The first scenario is a thin sanity check: does our app actually pull down the twitter search data feed and print out some message. We have no idea what it will print out, just the structure.

The second and all subsequent scenarios will start testing our executable within the ruby runtime, so we can stub out the remote HTTP calls.

Page 30: How to write tests/ stories for RubyGemsgotocon.com/dl/jaoo-sydney-2009/slides/NicWilliams_HowToDoWrite… · Since we’re at JAOO, we want to track conversations on twitter about

end result

$ rake install_gem$ tweettail jaooJAOO: 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 Housewa7son: To my Copenhagen Ruby or Java colleagues: Get to meet Ola Bini at JAOO Geek Nightsldaley: 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/-2B5jagwillis: @tweval I would give #jaoo a 10.0rowanb: Bags almost packed for Sydney. Scrum User Group then JAOO. Driving theremattnhodges: busy rest of week ahead. Spking @ Wiki Wed. Atlassian booth babe @ JAOO conference Syd Thurs & Fri. Kiama 4 Jase's wedding all w'end #fbpcalcado: 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...

28Thursday, 7 May 2009

Now we go and write some code, install the gem locally, and run it.

Page 31: How to write tests/ stories for RubyGemsgotocon.com/dl/jaoo-sydney-2009/slides/NicWilliams_HowToDoWrite… · Since we’re at JAOO, we want to track conversations on twitter about

When /^I run executable internally with arguments "(.*)"/ do |arguments| require 'rubygems' require File.dirname(__FILE__) + "/../../lib/tweet-tail" require "tweet-tail/cli"

@stdout = File.expand_path(File.join(@tmp_root, "executable.out")) in_project_folder do TweetTail::CLI.execute(@stdout_io = StringIO.new, arguments.split(" ")) @stdout_io.rewind File.open(@stdout, "w") { |f| f << @stdout_io.read } endend

‘I run executable internally’ step defn

29Thursday, 7 May 2009

New version where we can.

The first scenario is a thin sanity check: does our app actually pull down the twitter search data feed and print out some message. We have no idea what it will print out, just the structure.

The second and all subsequent scenarios will start testing our executable within the ruby runtime, so we can stub out the remote HTTP calls.

Page 32: How to write tests/ stories for RubyGemsgotocon.com/dl/jaoo-sydney-2009/slides/NicWilliams_HowToDoWrite… · Since we’re at JAOO, we want to track conversations on twitter about

Given /^a safe folder/ doGiven /^this project is active project folder/ doGiven /^env variable \$([\w_]+) set to "(.*)"/ do |env_var, value|Given /"(.*)" folder is deleted/ do |folder|

When /^I invoke "(.*)" generator with arguments "(.*)"$/ do |generator, args|When /^I run executable "(.*)" with arguments "(.*)"/ do |executable, args|When /^I run project executable "(.*)" with arguments "(.*)"/ do |executable, args|When /^I run local executable "(.*)" with arguments "(.*)"/ do |executable, args|When /^I invoke task "rake (.*)"/ do |task|

Then /^folder "(.*)" (is|is not) created/ do |folder, is|Then /^file "(.*)" (is|is not) created/ do |file, is|Then /^file with name matching "(.*)" is created/ do |pattern|Then /^file "(.*)" contents (does|does not) match \/(.*)\// do |file, does, regex|Then /^(does|does not) invoke generator "(.*)"$/ 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/ doThen /^task "rake (.*)" is executed successfully/ do |task|

Many provided steps

30Thursday, 7 May 2009

If you’ve used cucumber with rails you’ll have seen the provided steps for webrat like “When I follow ‘link’” and “When I select ‘some drop down’”. If you use the cucumber in a RubyGem you get many provided step definitions too.

The basic relationship between then is STDOUT and the file system. Do something which prints to STDOUT or modifies the file system, and then test the output or files.

Page 33: How to write tests/ stories for RubyGemsgotocon.com/dl/jaoo-sydney-2009/slides/NicWilliams_HowToDoWrite… · Since we’re at JAOO, we want to track conversations on twitter about

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 == textend

‘I should see...’features/step_definitions/common_steps.rb

31Thursday, 7 May 2009

To explore how this is happening, let’s look at it in reverse. When we want to test the STDOUT, we need to be able to view and explore it. So we’re reading the STDOUT from previous steps from a file. The file name is stored in @stdout.

The reason I store STDOUT into files is so that when a scenario fails, I can easily peruse each generated STDOUT file and look at what was output myself. If I just kept STDOUT in memory between steps then I’d lose that.

Page 34: How to write tests/ stories for RubyGemsgotocon.com/dl/jaoo-sydney-2009/slides/NicWilliams_HowToDoWrite… · Since we’re at JAOO, we want to track conversations on twitter about

When /^I run project executable "(.*)" with arguments "(.*)"/ do |executable, args| @stdout = File.expand_path(File.join(@tmp_root, "executable.out")) in_project_folder do system "ruby #{executable} #{arguments} > #{@stdout}" endend

When /^I invoke task "rake (.*)"/ do |task| @stdout = File.expand_path(File.join(@tmp_root, "tests.out")) in_project_folder do system "rake #{task} --trace > #{@stdout}" endend

‘When I do something...’

features/step_definitions/common_steps.rb

32Thursday, 7 May 2009

It is all the When steps that run commands or rake tasks or generators, which in turn creates new files and prints things out to STDOUT.

We run each of these commands from an external system command, just like the user would do, and then store the STDOUT to a file. Its file name is always stored in @stdout.

Page 35: How to write tests/ stories for RubyGemsgotocon.com/dl/jaoo-sydney-2009/slides/NicWilliams_HowToDoWrite… · Since we’re at JAOO, we want to track conversations on twitter about

‘Given a safe folder...’

Before do @tmp_root = File.dirname(__FILE__) + "/../../tmp" @home_path = File.expand_path(File.join(@tmp_root, "home")) FileUtils.rm_rf @tmp_root FileUtils.mkdir_p @home_path ENV["HOME"] = @home_pathend

features/support/env.rb

33Thursday, 7 May 2009

If we’re saving STDOUT to files, if we’re testing generators or other applications or libraries that create new files, where is a safe place to do that?

All scenarios get a “safe folder”. You get a tmp folder within your project source folder.

You even get a fake HOME folder and the $HOME env variable wired to it, incase your application wants to manipulate dot files or other content within a user’s home folder.

Page 36: How to write tests/ stories for RubyGemsgotocon.com/dl/jaoo-sydney-2009/slides/NicWilliams_HowToDoWrite… · Since we’re at JAOO, we want to track conversations on twitter about

tweettail jaoo -f

polling please?

how to test polling?34Thursday, 7 May 2009

Page 37: How to write tests/ stories for RubyGemsgotocon.com/dl/jaoo-sydney-2009/slides/NicWilliams_HowToDoWrite… · Since we’re at JAOO, we want to track conversations on twitter about

Scenario: Poll for results until app cancelled Given twitter has some search results for "jaoo" When I run executable internally with arguments "jaoo -f" Then I should see """ 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 """ When the sleep period has elapsed Then I should see """ 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... """ When I press "Ctrl-C" ...

features/cli.feature

35Thursday, 7 May 2009

Let’s write a scenario first for the -f option

The aim is to only very vaguely care about “how the hell am I going to implement ‘the sleep period has elapsed’

Page 38: How to write tests/ stories for RubyGemsgotocon.com/dl/jaoo-sydney-2009/slides/NicWilliams_HowToDoWrite… · Since we’re at JAOO, we want to track conversations on twitter about

adding -f option$ cucumber features/cli.feature:22 ... Scenario: Poll for results until app cancelled Given twitter has some search results for "jaoo" When I run executable internally with arguments "jaoo -f" invalid option: -f (OptionParser::InvalidOption)

module TweetTail::CLI def self.execute(stdout, arguments=[]) options = { :polling => false } parser = OptionParser.new do |opts| opts.on("-f", "Poll for new search results each 15 seconds." ) { |arg| options[:polling] = true } opts.parse!(arguments) end

app = TweetTail::TweetPoller.new(arguments.shift, options) app.refresh stdout.puts app.render_latest_results endend

lib/tweet-tail/cli.rb

36Thursday, 7 May 2009

Let’s drive development now. First, adding a -f option.

Page 39: How to write tests/ stories for RubyGemsgotocon.com/dl/jaoo-sydney-2009/slides/NicWilliams_HowToDoWrite… · Since we’re at JAOO, we want to track conversations on twitter about

features/fixtures/search-jaoo-since-1682666650.json

{ "results": [{ "text": "Wish I could be at #JAOO Australia...", "from_user": "CaioProiete", "id": 1711269079, }], "since_id": 1682666650, "refresh_url": "?since_id=1711269079&q=jaoo", "query": "jaoo"}

37Thursday, 7 May 2009

We’ll need some more sample data from twitter. Here’s what the JSON might look like on a subsequent call to the API. The since_id value in the file name comes from the original sample JSON data - the refresh_url value.

Page 40: How to write tests/ stories for RubyGemsgotocon.com/dl/jaoo-sydney-2009/slides/NicWilliams_HowToDoWrite… · Since we’re at JAOO, we want to track conversations on twitter about

features/step_definitions/twitter_data_steps.rb

Given /^twitter has some search results for "([^\"]*)"$/ do |query| FakeWeb.register_uri( :get, "http://search.twitter.com/search.json?q=#{query}", :file => File.expand_path(File.dirname(__FILE__) + "/../fixtures/search-#{query}.json")) since = "1682666650" FakeWeb.register_uri( :get, "http://search.twitter.com/search.json?since_id=#{since}&q=#{query}", :file => File.expand_path(File.dirname(__FILE__) + "/../fixtures/search-#{query}-since-#{since}.json"))end

38Thursday, 7 May 2009

Now wire in that sample file using fakeweb. This step is used by all the scenarios. If the refresh urls are never called by the application, the fakeweb stubbing doesn’t care.

Page 41: How to write tests/ stories for RubyGemsgotocon.com/dl/jaoo-sydney-2009/slides/NicWilliams_HowToDoWrite… · Since we’re at JAOO, we want to track conversations on twitter about

hmm, sleep...$ cucumber features/cli.feature:22 ... Scenario: Poll for results until app cancelled Given twitter has some search results for "jaoo" When I run executable internally with arguments "jaoo -f" Then I should see """ 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 """ When the sleep period has elapsed Then I should see """ 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... """

39Thursday, 7 May 2009

So we’ve cleared the way to test the polling mechanism.

But as I read this scenario, I can’t think of a way to run the app “a little bit” get its output, then continue, get its output, etc.

Page 42: How to write tests/ stories for RubyGemsgotocon.com/dl/jaoo-sydney-2009/slides/NicWilliams_HowToDoWrite… · Since we’re at JAOO, we want to track conversations on twitter about

Scenario: Poll for results until app cancelled Given twitter has some search results for "jaoo" When I run executable internally with arguments "jaoo -f" and wait 1 sleep cycle and quit Then I should see """ 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... """

features/cli.feature

40Thursday, 7 May 2009

So we refactor our scenario to represent something we _can_ test. Its still a nice, readable scenario. In fact its even shorter than before. Extra win!

Page 43: How to write tests/ stories for RubyGemsgotocon.com/dl/jaoo-sydney-2009/slides/NicWilliams_HowToDoWrite… · Since we’re at JAOO, we want to track conversations on twitter about

When /^I run executable internally with arguments "([^\"]*)" and wait (\d+) sleep cycles? and quit$/ do |args, cycles| hijack_sleep(cycles.to_i) When %Q{I run executable internally with arguments "#{args}"}end

features/step_definitions/executable_steps.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) endendWorld(TimeMachineHelper)

features/support/time_machine_helpers.rb

41Thursday, 7 May 2009

So our fancy new executable runner step still calls the same step as before; but first it hijacks the sleep() method; ultimately raising an Interrupt.

We’re using mocha here to stub out #sleep.

Page 44: How to write tests/ stories for RubyGemsgotocon.com/dl/jaoo-sydney-2009/slides/NicWilliams_HowToDoWrite… · Since we’re at JAOO, we want to track conversations on twitter about

require "mocha" World(Mocha::Standalone) Before do mocha_setupend After do begin mocha_verify ensure mocha_teardown endend

using mocha

features/support/mocha.rb

42Thursday, 7 May 2009

By default mocha’s setup and verification steps aren’t installed as we’re all used to in test/unit and rspec. In cucumber we need to explicitly set this up. So I put this code in its own support file.

Page 45: How to write tests/ stories for RubyGemsgotocon.com/dl/jaoo-sydney-2009/slides/NicWilliams_HowToDoWrite… · Since we’re at JAOO, we want to track conversations on twitter about

working!$ cucumber features/cli.feature:22Feature: 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 "jaoo" When I run executable internally with arguments "jaoo -f" and wait 1 sleep cycle and quit Then I should see """ 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... """

1 scenario (1 passed)3 steps (3 passed)

43Thursday, 7 May 2009

Once we go and write some code that continually loops and sleeps and loops and sleeps, our scenario will pass. Of course we’ll write some unit tests within this process too.

Page 46: How to write tests/ stories for RubyGemsgotocon.com/dl/jaoo-sydney-2009/slides/NicWilliams_HowToDoWrite… · Since we’re at JAOO, we want to track conversations on twitter about

44Thursday, 7 May 2009

I didn’t show much of the application code because that’s just a bit boring. Its all here on github. And the slides for this presentation, so that you can find the github url will be on slideshare. And a link to the slides on slideshare will be posted on my @drnic twitter account. You can’t _not_ find this code.

Page 47: How to write tests/ stories for RubyGemsgotocon.com/dl/jaoo-sydney-2009/slides/NicWilliams_HowToDoWrite… · Since we’re at JAOO, we want to track conversations on twitter about

task :default => [:features]Rakefile

Run unit tests + 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 "rubygems"; require "test/unit"; require "test/test_helper.rb"; require "test/test_tweet_poller.rb"' 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 "/Library/Ruby/Gems/1.8/gems/cucumber-0.3.2/lib:lib" ....................

5 scenarios (5 passed)17 steps (17 passed)

45Thursday, 7 May 2009

Its nice if the default rake task runs all your tests. Since we have unit tests and cucumber scenarios, add this line to your rakefile. Now “rake” will run both.

This turns out to be very useful when we turn on Continuous Integration.

Page 48: How to write tests/ stories for RubyGemsgotocon.com/dl/jaoo-sydney-2009/slides/NicWilliams_HowToDoWrite… · Since we’re at JAOO, we want to track conversations on twitter about

46Thursday, 7 May 2009

So let’s set up simple continuous integration. That way, each time we commit some new code, it will automatically trigger all your tests to be run.

Click edit.

Page 49: How to write tests/ stories for RubyGemsgotocon.com/dl/jaoo-sydney-2009/slides/NicWilliams_HowToDoWrite… · Since we’re at JAOO, we want to track conversations on twitter about

46Thursday, 7 May 2009

So let’s set up simple continuous integration. That way, each time we commit some new code, it will automatically trigger all your tests to be run.

Click edit.

Page 50: How to write tests/ stories for RubyGemsgotocon.com/dl/jaoo-sydney-2009/slides/NicWilliams_HowToDoWrite… · Since we’re at JAOO, we want to track conversations on twitter about

47Thursday, 7 May 2009

Click “Service Hooks”

Page 51: How to write tests/ stories for RubyGemsgotocon.com/dl/jaoo-sydney-2009/slides/NicWilliams_HowToDoWrite… · Since we’re at JAOO, we want to track conversations on twitter about

47Thursday, 7 May 2009

Click “Service Hooks”

Page 52: How to write tests/ stories for RubyGemsgotocon.com/dl/jaoo-sydney-2009/slides/NicWilliams_HowToDoWrite… · Since we’re at JAOO, we want to track conversations on twitter about

48Thursday, 7 May 2009

Page 53: How to write tests/ stories for RubyGemsgotocon.com/dl/jaoo-sydney-2009/slides/NicWilliams_HowToDoWrite… · Since we’re at JAOO, we want to track conversations on twitter about

48Thursday, 7 May 2009

Page 54: How to write tests/ stories for RubyGemsgotocon.com/dl/jaoo-sydney-2009/slides/NicWilliams_HowToDoWrite… · Since we’re at JAOO, we want to track conversations on twitter about

48Thursday, 7 May 2009

Page 55: How to write tests/ stories for RubyGemsgotocon.com/dl/jaoo-sydney-2009/slides/NicWilliams_HowToDoWrite… · Since we’re at JAOO, we want to track conversations on twitter about

49Thursday, 7 May 2009

The final step on github is to press the Test Hook link. But first, you’ll need to go

Page 56: How to write tests/ stories for RubyGemsgotocon.com/dl/jaoo-sydney-2009/slides/NicWilliams_HowToDoWrite… · Since we’re at JAOO, we want to track conversations on twitter about

1. Create account with http://runcoderun.com2. Press ‘Test Hook’

49Thursday, 7 May 2009

The final step on github is to press the Test Hook link. But first, you’ll need to go

Page 57: How to write tests/ stories for RubyGemsgotocon.com/dl/jaoo-sydney-2009/slides/NicWilliams_HowToDoWrite… · Since we’re at JAOO, we want to track conversations on twitter about

1. Create account with http://runcoderun.com2. Press ‘Test Hook’

49Thursday, 7 May 2009

The final step on github is to press the Test Hook link. But first, you’ll need to go

Page 58: How to write tests/ stories for RubyGemsgotocon.com/dl/jaoo-sydney-2009/slides/NicWilliams_HowToDoWrite… · Since we’re at JAOO, we want to track conversations on twitter about

50Thursday, 7 May 2009

Page 59: How to write tests/ stories for RubyGemsgotocon.com/dl/jaoo-sydney-2009/slides/NicWilliams_HowToDoWrite… · Since we’re at JAOO, we want to track conversations on twitter about

50Thursday, 7 May 2009

Page 60: How to write tests/ stories for RubyGemsgotocon.com/dl/jaoo-sydney-2009/slides/NicWilliams_HowToDoWrite… · Since we’re at JAOO, we want to track conversations on twitter about

51Thursday, 7 May 2009

So there’s an epilogue to this story and this application. The moment I finished, flush with confidence that my integration-test-driven application was working, I pushed the gem to rubyforge and then announced it on twitter. Within minutes I got a bug report.

So testing still isn’t a holy grail. An idiot software developer can’t change his spots apparently.

Page 61: How to write tests/ stories for RubyGemsgotocon.com/dl/jaoo-sydney-2009/slides/NicWilliams_HowToDoWrite… · Since we’re at JAOO, we want to track conversations on twitter about

51Thursday, 7 May 2009

So there’s an epilogue to this story and this application. The moment I finished, flush with confidence that my integration-test-driven application was working, I pushed the gem to rubyforge and then announced it on twitter. Within minutes I got a bug report.

So testing still isn’t a holy grail. An idiot software developer can’t change his spots apparently.

Page 62: How to write tests/ stories for RubyGemsgotocon.com/dl/jaoo-sydney-2009/slides/NicWilliams_HowToDoWrite… · Since we’re at JAOO, we want to track conversations on twitter about

51Thursday, 7 May 2009

So there’s an epilogue to this story and this application. The moment I finished, flush with confidence that my integration-test-driven application was working, I pushed the gem to rubyforge and then announced it on twitter. Within minutes I got a bug report.

So testing still isn’t a holy grail. An idiot software developer can’t change his spots apparently.

Page 63: How to write tests/ stories for RubyGemsgotocon.com/dl/jaoo-sydney-2009/slides/NicWilliams_HowToDoWrite… · Since we’re at JAOO, we want to track conversations on twitter about

52Thursday, 7 May 2009

For more generic information about Cucumber, start at its home page, watch some videos, etc.

To learn more about testing RubyGems, read other people’s gems. Read the newgem code base which self-tests with its own helpers.

Page 64: How to write tests/ stories for RubyGemsgotocon.com/dl/jaoo-sydney-2009/slides/NicWilliams_HowToDoWrite… · Since we’re at JAOO, we want to track conversations on twitter about

How to write tests/stories for RubyGems

Dr Nic Williamsmocra.com

drnicwilliams.comtwitter: @drnic

[email protected]

53Thursday, 7 May 2009

And if you have any questions about anything, you can attempt to get help by contacting me.