Rails on the Run

Rails experiments by Matt Aimonetti

Browsing Posts tagged BDD

I haven’t posted for quite a long time. The thing is I moved to a new place and I’m really busy on working clients + setting up my new office + dealing with way too much paperwork.

Anyway, enough excuses, here are few tips that I believe will be useful to some of you:

ZenTest Autotest

I love autotest, but you might have noticed that sometimes (especially on big projects), ZenTest might start using more CPU than expected. On my machine, that results in the fan going off and annoying the crap out of me.

The solution is quite simple, exclude all folders you don’t need to monitor. To do that, update ZenTest to version 3.8.X

sudo gem update ZenTest

(older version had a different syntax)

Now, edit your .autotest that should be located in ~/.autotest (if it doesn’t exist, create it).

Finally add the following code:

1
2
3
4

  Autotest.add_hook :initialize do |at|
    %w{.svn .hg .git vendor}.each {|exception| at.add_exception(exception)}
  end

I personally freeze rails in vendor and I autotest is way happier when it doesn’t have to monitor some extra files. (note that we also exclude folders such as .git or .svn)
(you can also include files etc… read more there)

RSpec

RSpec is certainly my favorite Ruby tool and I’m glad to say that most of my SD.rb friends finally got convinced!

Now, few people complained to me about spec failures outputting the full stack such as:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26

 The Sessions controller should fail since it's a test' FAILED
 expected true, got false
 test_app-git/trunk/vendor/plugins/rspec/lib/spec/expectations.rb:52:in `fail_with'
 test_app-git/trunk/vendor/plugins/rspec/lib/spec/expectations/handler.rb:21:in `handle_matcher'
 test_app-git/trunk/vendor/plugins/rspec/lib/spec/expectations/extensions/object.rb:34:in `should'
 ./spec/controllers/sessions_controller_spec.rb:25:
 test_app-git/trunk/vendor/plugins/rspec/lib/spec/example/example_methods.rb:78:in `instance_eval'
 test_app-git/trunk/vendor/plugins/rspec/lib/spec/example/example_methods.rb:78:in `run_with_description_capturing'
 test_app-git/trunk/vendor/plugins/rspec/lib/spec/example/example_methods.rb:19:in `execute'
 /opt/local/lib/ruby/1.8/timeout.rb:48:in `timeout'
 test_app-git/trunk/vendor/plugins/rspec/lib/spec/example/example_methods.rb:16:in `execute'
 test_app-git/trunk/vendor/plugins/rspec/lib/spec/example/example_group_methods.rb:288:in `execute_examples'
 test_app-git/trunk/vendor/plugins/rspec/lib/spec/example/example_group_methods.rb:287:in `each'
 test_app-git/trunk/vendor/plugins/rspec/lib/spec/example/example_group_methods.rb:287:in `execute_examples'
 test_app-git/trunk/vendor/plugins/rspec/lib/spec/example/example_group_methods.rb:121:in `run'
 test_app-git/trunk/vendor/plugins/rspec/lib/spec/runner/example_group_runner.rb:22:in `run'
 test_app-git/trunk/vendor/plugins/rspec/lib/spec/runner/example_group_runner.rb:21:in `each'
 test_app-git/trunk/vendor/plugins/rspec/lib/spec/runner/example_group_runner.rb:21:in `run'
 test_app-git/trunk/vendor/plugins/rspec/lib/spec/runner/options.rb:89:in `run_examples'
 test_app-git/trunk/vendor/plugins/rspec/lib/spec/runner/command_line.rb:19:in `run'
 script/spec:4:

  Finished in 6.035147 seconds

  400 examples, 1 failure

We can really easily change that, open you spec.opts file located in your spec folder.

it probably looks like that:

1
2
3
4
5
6
7
8

  --colour
  --format
  progress
  --loadby
  mtime
  --reverse
  --backtrace

Get rid of “–backtrace” and your new failure should look like:

1
2
3
4
5
6
7
8
9
10
11

  1)
  'The Sessions controller The Sessions controller should fail since it's a test' FAILED
  expected false, got true
  ./spec/controllers/sessions_controller_spec.rb:25:
  script/spec:4:

  Finished in 0.269956 seconds

  15 examples, 1 failure
  

Other stuff you may find interesting (in no particular order):

A bit more than a month ago I posted a tutorial on how to use Flash with Rails to create some awesome/sexy graphs.

chart

Since a lot of people seemed interested by the topic, the SDRuby guys asked me to do a intro talk on how to create Sexy Charts with super sexy Rails.

In the mean time, a lot of people were asking for a example app to look at. People knowing me know that I’m quite lazy and I don’t like repeating tasks. I therefore decided to kill 2 birds with one stone and wrote a demo app that I would use during my presentation

As I was writing the demo app, I quickly realized that my talk would be even sexier if I would show some best practices. After all, an introduction talk is meant to help newbies learning the tricks that will change them in ninjas!

Sexy charts are sexy now, but in 15 years they might not look so sexy anymore. However BDD is super hot now and will always be sexy! (even though we’ll probably adopt other even hotter approaches).

Based on the circumstances I decided that Sexy charts would become an excuse to show people how to do BDD using RSpec and how to test a XML view as described in this previous post

During my presentation I totally forgot to show people what what kind of XML we were trying to feed amCharts, so here is the file:

http://pastie.caboo.se/120055

The code used in the presentation is also available here

Presentation available here (the sound is a bit saturated, sorry about that. Note that we made the video big enough so you can follow with the code if you don’t understand my accent :) )

Feel free to watch the other SDRuby podcasts or even better, subscribe to our feed.

Next SDRuby meeting will be Thursday, December 6 @ 7:30pm

Location: UCSD CS Building

We’ll be talking about Unobtrusive Javascript, the Facebook API, and hosting our first Rails Roundtable.

Newbies and experts welcome!

As a good Rubyist, I do TDD and even BDD.

Since I’ve started using RSpec I’ve started writing tests against my views. RSpec makes things really easy and I’ve been enjoying testing my views.

I’m not the only one having fun, check this great post from Mr Planet Argon aka Robby Russel

Recently I was working on implementing some Sexy Charts and I was using a XML builder to create an XML view of for a controller. Since I wanted to be a good Rails Ninja and obey the BDD rules, I figured I needed to test my XML view. Making sure that the nodes and the attributes were properly created. Turned out that is wasn’t too hard, there was many options but none were very well documented so I decided to write this quick tutorial.

UPDATE 31 Oct 2007: After a comment from Josh Knowles, I updated the tests to test with have_tags (built in RSpec) and hpricot.

Hpricot

hpricot is a awesome HTML parser perfect for screen scraping. But wait, there’s more to this awesome library, hpricot can also parse XML.

If you watched the excellent RSpec peepcasts you probably noticed that topfunky aka Geoffrey Grosenbach uses hpricot to test a remote API.

In our case, we’ll use hpricot to test that our generated XML follows our expectations.

XML Builder + RSpec

Let’s write a quick test to make sure our controller uses a XML builder view:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
  describe AveragesController, "handling GET /averages.xml" do

    before do
      Average.stub!(:find).and_return(@average)
    end
  
    def do_get
      @request.env["HTTP_ACCEPT"] = "application/xml"
      get :index
    end
  
    it "should render the action using the XML builder" do
      do_get
      response.should render_template('averages/index.xml.builder')
    end

  end

To make this example pass, we need to modify our rspec generated controller.

1
2
3
4
5
6
7
8
  def index
    @averages = Average.find(:all)
  
    respond_to do |format|
      format.html # index.html.erb
      format.xml  { render :action => "index.xml.builder", :layout => false }
    end
  end

(Please note that I’m using Rails 2.0 and that’s why I’m not using a .rxml view)

Here is what our XML file should end up looking like:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
  <?xml version="1.0" encoding="UTF-8"?>
  <chart>
    <series>
      <value xid="0">January</value>
      <value xid="1">February</value>
      <value xid="2">March</value>
      <value xid="3">April</value>
      <value xid="4">May</value>

      <value xid="5">June</value>
      <value xid="6">July</value>
      <value xid="7">August</value>
      <value xid="8">September</value>
      <value xid="9">October</value>
      <value xid="10">November</value>

      <value xid="11">December</value>
    </series>
    <graphs>
      <graph fill_alpha="50" color="#FF0000" fill_color="#CC0000" title="high">
        <value xid="0">65.1</value>
        <value xid="1">65.7</value>
        <value xid="2">64.9</value>

        <value xid="3">66.7</value>
        <value xid="4">67.1</value>
        <value xid="5">69.3</value>
        <value xid="6">73.0</value>
        <value xid="7">74.8</value>
        <value xid="8">75.4</value>

        <value xid="9">73.4</value>
        <value xid="10">68.9</value>
        <value xid="11">65.3</value>
      </graph>
      <graph fill_alpha="50" color="#0000CC" fill_color="#0000CC" title="low">
        <value xid="0">48.9</value>
        <value xid="1">50.7</value>

        <value xid="2">52.9</value>
        <value xid="3">55.6</value>
        <value xid="4">59.2</value>
        <value xid="5">61.9</value>
        <value xid="6">65.7</value>
        <value xid="7">67.3</value>

        <value xid="8">65.7</value>
        <value xid="9">61.0</value>
        <value xid="10">54.0</value>
        <value xid="11">48.7</value>
      </graph>
    </graphs>
  </chart>
  

Let’s write some tests to make sure our view is ok:

index.xml.builder_spec.rb

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
require File.dirname(__FILE__) + '/../../spec_helper'
require 'hpricot'

describe "/averages/index.xml.builder" do
  include AveragesHelper
  
  before do
    average_1 = mock_model(Average)
    average_1.stub!(:month).and_return("January")
    average_1.stub!(:high).and_return("74.5")
    average_1.stub!(:low).and_return("61.5")
    average_2 = mock_model(Average)
    average_2.stub!(:month).and_return("February")
    average_2.stub!(:high).and_return("82.5")
    average_2.stub!(:low).and_return("71.5")

    assigns[:averages] = [average_1, average_2]
  end

  it "should render the months in the series" do
    render "/averages/index.xml.builder"
    response.should have_tag("value", 'January')
    response.should have_tag("value", 'February')
    # Same thing but with Hpricot
    doc = Hpricot.XML(response.body.to_s)
    (doc/:value).first.inner_html.should == 'January'
    (doc/:value)[1].inner_html.should == 'February'
  end
  
  it "should set the xid attributes for the series" do
    render "/averages/index.xml.builder"
    response.should have_tag("value[xid=0]:first-child")
    response.should have_tag("value[xid=1]:last-child")
    # Same thing but with Hpricot
    doc = Hpricot.XML(response.body.to_s)
    (doc/:value).first["xid"].should == '0'
    (doc/:value).last["xid"].should == '1'
  end
  
  it "should have 2 graphs and they should have a title" do
    render "/averages/index.xml.builder"
    response.should have_tag("graph[title=high]:first-child")
    response.should have_tag("graph[title=low]:last-child")
    # Same thing but with Hpricot
    doc = Hpricot.XML(response.body.to_s)
    (doc/:graph).size.should == 2
    (doc/:graph).first["title"].should == 'high'
    (doc/:graph).last["title"].should == 'low'
  end
  
  it "should have a color set by graph" do
    render "/averages/index.xml.builder"
    response.should have_tag("graph[color]:first-child")
    response.should have_tag("graph[color]:last-child")
    response.should have_tag("graph[fill_color]:last-child")
    response.should have_tag("graph[fill_alpha]:last-child")
    # Same thing but with Hpricot
    doc = Hpricot.XML(response.body.to_s)
    (doc/:graph).first["color"].should_not be_nil
    (doc/:graph).last["color"].should_not be_nil
    (doc/:graph).last["fill_color"].should_not be_nil
    (doc/:graph).last["fill_alpha"].should_not be_nil
  end
  
  it "should have an xid for each graph value" do
    render "/averages/index.xml.builder"
    response.should have_tag("graph > value[xid=0]:first-child")
    # Same thing but with Hpricot
    doc = Hpricot.XML(response.body.to_s)
    (doc/:graph/:value).first["xid"].should == "0"
  end
  
  it "should have the high average as values of the first graph" do
    render "/averages/index.xml.builder"
    response.should have_tag("graph > value:first-child", "74.5")
    # Same thing but with Hpricot
    doc = Hpricot.XML(response.body.to_s)
    (doc/:graph/:value).first.inner_html.should == "74.5"
  end
  
end

The first thing you must do (after installing the hpricot gem) is to require hpricot in your test:


  require 'hpricot'

Now that hpricot is created we can use it to parse the response and check against our expectations.

(we create mock objects to pass to the view so we know exactly what to expect and we separate Model/Controller/Views tests)

To check against our response we have to use hpricot parser syntax. It might look at bit funny at first, but believe me it’s really easy once you get it.

But first, let’s parse the view:

1
2
3
4
# Render the mocked up data using the xml view
render "/averages/index.xml.builder"
# Load and parse the view response body:
doc = Hpricot.XML(response.body.to_s)  

Let’s look at the first test:

1
2
3
4
5
6
  it "should render the months in the series" do
    render "/averages/index.xml.builder"
    doc = Hpricot.XML(response.body.to_s)
    (doc/:value).first.inner_html.should == 'January'
    (doc/:value)[1].inner_html.should == 'February'
  end

(doc/:value) returns all the value nodes, we take the first one and extract its content. We expect that it would match the name of the month for the first average.

Let’s now look at another test:

1
2
3
4
5
6
7
  it "should have 2 graphs and they should have a title" do
    render "/averages/index.xml.builder"
    doc = Hpricot.XML(response.body.to_s)
    (doc/:graph).size.should == 2
    (doc/:graph).first["title"].should == 'high'
    (doc/:graph).last["title"].should == 'low'
  end

The thing to look at here is the fact that we are checking on the node’s attribute “title”. Really simple syntax and clean test, isn’t it?

Finally let’s look at the last example:

1
2
3
4
5
  it "should have the high average as values of the first graph" do
    render "/averages/index.xml.builder"
    doc = Hpricot.XML(response.body.to_s)
    (doc/:graph/:value).first.inner_html.should == "74.5"
  end

We are checking that the content of the first value node nested inside a graph node is equal to 74.5 which is the high average for the first month.

In practice, you probably won’t write all these tests at once, but anyway, let’s look at our XML builder which will make all these tests pass:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
xml.instruct!  :x ml, :version=>"1.0", :encoding=>"UTF-8"
xml.chart do
  xml.series do    
    @averages.each_with_index do |average, index|
      xml.value average.month,  :x id => index
    end
  end
  
  xml.graphs do
    xml.graph :title => 'high', :color => "#FF0000", :fill_alpha => "50", :fill_color => "#CC0000" do
      @averages.each_with_index do |average, index|
        xml.value average.high,  :x id => index
      end
    end
    
    xml.graph :title => 'low', :color => "#0000CC", :fill_alpha => "50", :fill_color => "#0000CC" do
      @averages.each_with_index do |average, index|
        xml.value average.low,  :x id => index
      end
    end
  end
  
end

Hpricot is a really nice tool which can make your BDD life much easier. And even if you don’t do BDD/TDD yet, it’s a great way to verify that any XML data you receive/generate is valid.

Happy testing