My conscience is leading me more and more into formal testing and I have been trying out cucumber the latest ruby Behaviour Driven Development offering. Cucumber is still very new and there is little documentation available so here are some notes.
The basic idea of cucumber is along the lines that your tests are almost in spoken language rather that a programming language and hence non technical people like the marketing department, clients etc can read them and understand them, although I just cannot believe that they could ever write them in any useful way. The tests describe the behaviour of the systems and hence document it in a similar way to a requirements specification. If one decided, for example, to go with the herd one could take the behaviour listings as a specification for reimplementing everything in .NET.
Or something like that.
Anyway, here is a cucumber 'feature' that I have come up with for pctv. This describes the features of the system. Pctv shows a list of tv programs, one can click on a program and see a list of episodes of that program.
Feature: Looking at programme listing
In order to watch programs
I should be able to
See listings of what is on
Scenario: Look at programs
Given I have some programmes
And I visit "/"
Then I should see "Dora the Explorer"
And I should see "Loose Women"
Scenario: Examine Dora the Explorer
Given I have some programmes
And I visit "/"
When I follow "Dora the Explorer"
Then I should see "Dora the Explorer"
And I should see "Tomorrows Episode"
And I should not see "Yesterdays Episode"
And I should not see "Loose Women"
And the "Series Link" checkbox should not be checked
Scenario: Mark Dora series link
Given I have some programmes
And I visit "/"
When I follow "Dora the Explorer"
And I check "Series Link"
And I press "Update"
Then I should see "Dora the Explorer"
And I should see "Tomorrows Episode"
And I should not see "Yesterdays Episode"
And I should not see "Loose Women"
And the "Series Link" checkbox should be checked
Scenario: Star Trek series link
Given I have some programmes
And I visit "/"
When I follow "Star Trek"
Then I should see "Star Trek"
And the "Series Link" checkbox should be checked
And I should see "Boldly Gone"
And I should see "Boldly Going"
And I should not see "Yesterdays Episode"
And I should not see "Loose Women"
Scenario: Unmark Star Trek series link
Given I have some programmes
And I visit "/"
When I follow "Star Trek"
And I uncheck "Series Link"
And I press "Update"
Then I should see "Star Trek"
And the "Series Link" checkbox should not be checked
And I should see "Boldly Gone"
And I should see "Boldly Going"
And I should not see "Yesterdays Episode"
And I should not see "Loose Women"
Scenario: Visit recordings page
Given I have some programmes
And I visit "/"
When I follow "Recordings"
Then I should see "Star Trek"
And I should see "Boldly Gone"
And I should see "Recorded"
And I should not see "Boldly Going"
Scenario: Examine Categories of Women
Given I have some programmes
And I visit "/"
When I follow "Women"
Then I should see "Loose Women"
And I should not see "Dora the Explorer"
And I should not see "Star Trek"
Scenario: Search for Dora
Given I have some programmes
And I visit "/programmes"
When I follow "Search"
And I fill in "Search" with "Dora"
And I press "Seek"
Then I should see "Dora the Explorer"
And I should not see "Loose Women"
And I should not see "Star Trek"
I think this works at the level of being quite readable and describes nicely how one interacts with pctv.
This uses the webrat gem which simulates the interactions with a web browser (seeing text, clicking links etc).
The Given, When and Then keywords translate directly to code steps which establish the state of the system, an event that changes the system and the results of the change respectively. When cucumber is set up in a rails application with:
script/generate cucumber
it will generate a file called /features/step_definitions/webrat_steps.rb which contains some useful webrat examples such as:
When /^I press "(.*)"$/ do |button|
clicks_button(button)
end
Then /^I should see "(.*)"$/ do |text|
response.body.should =~ /#{text}/m
end
So the When term is converted into webrat operations and the Then term examines the html generated by the code. It should be noted that webrat does nothing in respect to examining the output, that can be done directly on the response object or it can be done using rspecs have_tag method:
Then /it should have the tag "(.*)" containing the text "(.*)"/ do |strTag, strText|
response.should have_tag( strTag, /#{strText}/)
end
The above is one of mine and I'm a bit uneasy about it. I'm not sure if the cucumber level of testing should be above the implemntation and above the level of examining the html in the response or whether it should be at the level of what a user would actually see on a web site. The studying of the html should be at the level of view tests using rspec-rails but this is just a feeling.
Here is another one of mine:
Given /^I visit "(.*)"$/ do |url|
visits(url)
end
This seems like an obvious one to need and I'm not sure why the generated webrat steps don't include it, it makes me wonder if I haven't quite grasped the philosophy.
Cucumber doesn't seem to support fixtures and googling for this I came across a thread where someone was asking about this issue and the author of cucumber said 'google for it': there is no FM to R. Other posters in the thread were more helpful but I still found that the fixtures were only loaded in the first scenario and for following scenarios the database stayed empty. I decided to just code something:
1 Given /I have some programmes/ do
2 strYesterday = (DateTime.now - 1).strftime( "%Y%m%d")
3 strTomorrow = (DateTime.now + 1).strftime( "%Y%m%d")
4
5 oChannel1 = Channel.create( :dvbname => 'Nick Jr')
6 oChannel2 = Channel.create( :dvbname => 'ITV ONE')
7
8 oCategory1 = Category.create( :name => 'Children')
9 oCategory2 = Category.create( :name => 'Crap')
10
11 oProgram = Programme.create( :title => "Dora the Explorer", :category => oCategory1)
12 oEpisode = Episode.create( :programme => oProgram, :subtitle => "Episode 1", :description => "Yesterdays Episode", :channel => oChannel1, :starting => DateTime.parse( strYesterday + "080000"), :ending => DateTime.parse( strYesterday + "080000"))
13 oEpisode2 = Episode.create( :programme => oProgram, :subtitle => "Episode 2", :description => "Tomorrows Episode", :channel => oChannel1, :starting => DateTime.parse( strTomorrow + "080000"), :ending => DateTime.parse( strTomorrow + "080000"))
14
15 oProgram2 = Programme.create( :title => "Loose Women", :category => oCategory2)
16 oEpisode2 = Episode.create( :programme => oProgram2, :description => "You did it where?", :channel => oChannel2, :starting => DateTime.parse( strTomorrow + "123000"), :ending => DateTime.parse( strTomorrow + "133000"))
17
18 oProgram3 = Programme.create( :title => "Star Trek", :category => oCategory1, :series_link => true)
19 oEpisode1 = Episode.create( :programme => oProgram3, :description => "Boldly Gone", :channel => oChannel2, :starting => DateTime.parse( strTomorrow + "013000"), :ending => DateTime.parse( strYesterday + "023000"),
20 :recording_status => 'recorded', :filename => 'Star Trek.avi')
21 oEpisode2 = Episode.create( :programme => oProgram3, :description => "Boldly Going", :channel => oChannel2, :starting => DateTime.parse( strTomorrow + "013000"), :ending => DateTime.parse( strTomorrow + "023000"))
22 end
Toggle Line Numbers
I put the following in /features/support/env.rb
require "webrat/rails"
and run it using
spec features
and I get:
1 Feature: Looking at programme listing
2 In order to watch programs
3 I should be able to
4 See listings of what is on
5 Scenario: Look at programs
6 Given I have some programmes
7 And I visit "/"
8 Then I should see "Dora the Explorer"
9 And I should see "Loose Women"
10
11 Scenario: Examine Dora the Explorer
12 Given I have some programmes
13 And I visit "/"
14 When I follow "Dora the Explorer"
15 Then I should see "Dora the Explorer"
16 And I should see "Tomorrows Episode"
17 And I should not see "Yesterdays Episode"
18 And I should not see "Loose Women"
19 And the "Series Link" checkbox should not be checked
20
21 Scenario: Mark Dora series link
22 Given I have some programmes
23 And I visit "/"
24 When I follow "Dora the Explorer"
25 And I check "Series Link"
26 And I press "Update"
27 Then I should see "Dora the Explorer"
28 And I should see "Tomorrows Episode"
29 And I should not see "Yesterdays Episode"
30 And I should not see "Loose Women"
31 And the "Series Link" checkbox should be checked
32
33 Scenario: Star Trek series link
34 Given I have some programmes
35 And I visit "/"
36 When I follow "Star Trek"
37 Then I should see "Star Trek"
38 And the "Series Link" checkbox should be checked
39 And I should see "Boldly Gone"
40 And I should see "Boldly Going"
41 And I should not see "Yesterdays Episode"
42 And I should not see "Loose Women"
43
44 Scenario: Unmark Star Trek series link
45 Given I have some programmes
46 And I visit "/"
47 When I follow "Star Trek"
48 And I uncheck "Series Link"
49 And I press "Update"
50 Then I should see "Star Trek"
51 And the "Series Link" checkbox should not be checked
52 And I should see "Boldly Gone"
53 And I should see "Boldly Going"
54 And I should not see "Yesterdays Episode"
55 And I should not see "Loose Women"
56
57 Scenario: Visit recordings page
58 Given I have some programmes
59 And I visit "/"
60 When I follow "Recordings"
61 Then I should see "Star Trek"
62 And I should see "Boldly Gone"
63 And I should see "Recorded"
64 And I should not see "Boldly Going"
65
66 Scenario: Examine Categories of Women
67 Given I have some programmes
68 And I visit "/"
69 When I follow "Women"
70 Then I should see "Loose Women"
71 And I should not see "Dora the Explorer"
72 And I should not see "Star Trek"
73
74 Scenario: Search for Dora
75 Given I have some programmes
76 And I visit "/programmes"
77 When I follow "Search"
78 And I fill in "Search" with "Dora"
79 Could not find field labeled "Search" (RuntimeError)
80 /usr/lib/ruby/gems/1.8/gems/webrat-0.3.2/lib/webrat/core/flunk.rb:4:in `flunk'
81 /usr/lib/ruby/gems/1.8/gems/webrat-0.3.2/lib/webrat/core/locators.rb:16:in `field_labeled'
82 /usr/lib/ruby/gems/1.8/gems/webrat-0.3.2/lib/webrat/core/locators.rb:10:in `field'
83 /usr/lib/ruby/gems/1.8/gems/webrat-0.3.2/lib/webrat/core/scope.rb:183:in `locate_field'
84 /usr/lib/ruby/gems/1.8/gems/webrat-0.3.2/lib/webrat/core/scope.rb:41:in `fills_in'
85 /usr/lib/ruby/gems/1.8/gems/webrat-0.3.2/lib/webrat/rails.rb:88:in `send'
86 /usr/lib/ruby/gems/1.8/gems/webrat-0.3.2/lib/webrat/rails.rb:88:in `method_missing'
87 /usr/lib/ruby/gems/1.8/gems/actionpack-2.2.2/lib/action_controller/integration.rb:498:in `__send__'
88 /usr/lib/ruby/gems/1.8/gems/actionpack-2.2.2/lib/action_controller/integration.rb:498:in `method_missing'
89 ./features/step_definitions/webrat_steps.rb:15:in `And /^I fill in "(.*)" with "(.*)"$/'
90 features/programmes.feature:79:in `And I fill in "Search" with "Dora"'
91 And I press "Seek" # features/step_definitions/webrat_steps.rb:6
92 Then I should see "Dora the Explorer" # features/step_definitions/webrat_steps.rb:85
93 And I should not see "Loose Women" # features/step_definitions/webrat_steps.rb:89
94 And I should not see "Star Trek" # features/step_definitions/webrat_steps.rb:89
95 And I press "Seek" # features/step_definitions/webrat_steps.rb:6
96 Then I should see "Dora the Explorer" # features/step_definitions/webrat_steps.rb:85
97 And I should not see "Loose Women" # features/step_definitions/webrat_steps.rb:89
98 And I should not see "Star Trek" # features/step_definitions/webrat_steps.rb:89
99
100
101 58 steps passed
102 1 steps failed
103 4 steps skipped
Toggle Line Numbers
The error above is because webrat can't find the Search box in my form, although it looks good to me. Not the easist thing to debug, dump the html into the console and peer at it, viewing source in a web browser is easier. Webrat has a function save_and_open_page that only seems to work in OSX.
UPDATE: found the problem with the search term. My search form was set up like this:
<% form_tag(:controller => :programmes, :action => :search) do %>
<p>
<%= label_tag "Search" %> <%= text_field_tag :search_term %>
<%= submit_tag "Seek" %>
</p>
<% end %>
I kinda assumed that webrat used the fact that the label was before the text field in the html to associate the two but it turns out there is a correct way to do this association. The following works, the :search_term parameter to label_tag makes it generate a 'for' attribute that points to the text input field:
<% form_tag(:controller => :programmes, :action => :search) do %>
<p>
<%= label_tag :search_term, "Search" %> <%= text_field_tag :search_term %>
<%= submit_tag "Seek" %>
</p>
<% end %>
Webrat is helping me validate my html.
I still have a few problems with this:
-
in the example of a checkbox, I can set up a test to check a checkbox and I can see it checked but as an engineer I'm itching to look in the database to make sure it is checked. I have to resist the urge to test at the cucumber level and tell myself the view, controller and model tests would have that sorted.
-
to test that a checkbox can be unchecked I stuffed the database with a record that is already checked. I could do these steps:
Scenario: Mark Dora series link
Given I have some programmes
And I visit "/"
When I follow "Dora the Explorer"
And I check "Series Link"
And I press "Update"
And I uncheck "Series Link"
And I press "Update"
Then I should see "Dora the Explorer"
And I should see "Tomorrows Episode"
And I should not see "Yesterdays Episode"
And I should not see "Loose Women"
And the "Series Link" checkbox should not be checked
and I have to rely on having already tested that checking works. Writing now and thinking more, this seems ok.
-
my specs are lacking in justifications. This happens in most specifications, things are detailed with no details on why things are this way. For example, in my test I click on 'Dora the Explorer' and I don't see 'Loose Women' because clicking on Dora the Explorer takes me to the episodes of Dora and Dora doesn't usually feature loose women. The test above doesn't state this unless I add comments which breaks the idea of the formal definition language. Maybe it needs something like 'because'?
Then I should not see "Loose Women" Because that is not an episode of Dora
And I should not see "Yesterdays Episode" Because it should only show future episodes
However since I can define the Then terms myself there is nothing stopping me adding Because to my tests and this could then force me to put in my justifications!
-
tests where you don't want to see something are fragile. I could be happy that I don't see "Yesterdays Episode" and then I later break things to that the text "Yesterday's Episode" is generated but the test still passes.
I can definitely see some use for this in keeping auditors happy.