This is part of the Chores series of posts
While committing Chores to git after the last refactoring, I’m thinking to myself. Who am I kidding? I’m going to write a post on functional testing? But I suck at functional testing! Well, we’ll see how it goes.
First, I’d like to install a gem called Machinist (this is also available as a plugin, if you prefer). Machinist is a drop in replacement for fixtures, allowing you to make (save) validated test data with a little config and a method call. You can read all about it in the readme, linked above. To install the gem simply:
$ gem install notahat-machinist
And be sure to configure your gem by adding the following to config/environments/test.rb
config.gem "notahat-machinist", :lib => 'machinist', :version => '0.3.1'
In fact, something I failed to cover is configuring your gems. Something you might want to take a minute to do.
Blueprints
Machinist uses “blueprints” to create records in the database for testing. These blueprints tell machinist how to create a valid record / object. These are stored in a blueprints file. So lets create a file in test/ called blueprints.rb that contains:
1
2
3
| Chore.blueprint do
description "Put the dirty clothes in the hamper"
end |
and require the blueprint file in our test helper (test/test_helper.rb)
1
2
| require 'machinist'
require File.expand_path(File.dirname(__FILE__) + "/blueprints") |
On to the functional tests, or testing of the controllers. I used the rails scaffold functional tests as the starting point for my functional tests for chore. Here’s how the first run ended up. You might notice that there is not test for update, that’s because I’m not sure update is needed at the moment and since our cucmber feature (remember we are trying to get that working) didn’t say anything about updating, I’m just leaving it off for now.
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
| require 'test_helper'
class ChoresCanCrud < ActionController::TestCase
tests ChoresController
specify "test that it should respond to index" do
get :index
assert_response :success
assert_not_nil assigns(:chores)
end
specify "that it should pass chore on new" do
get :new
assert_response :success
assert_not_nil assigns(:chore)
end
specify "that it should create chore" do
assert_difference 'Chore.count' do
post :create, :chore => { :description => 'My Chore' }
end
assert_redirected_to chores_url
end
specify "that it will destroy a chore, muahahahahaha" do
chore = Chore.make
assert_difference 'Chore.count', -1 do
delete :destroy, :id => chore.id
end
assert_redirected_to chores_url
end
end |
The corresponding Chores controller looks like this
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
| class ChoresController < ApplicationController
def index
@chores = Chore.find(:all)
end
def new
@chore = Chore.new
end
def create
@chore = Chore.new(params[:chore])
@chore.save
redirect_to chores_url
end
def destroy
chore = Chore.find(params[:id])
chore.destroy
redirect_to chores_url
end
end |
Additionally, I had to make a couple views to get the tests to run. They are skeletal at this point, barely more than an empty file. In fact ./app/views/chores/index.html.erb is an empty file and ./app/views/chores/new.html.erb is
1
2
3
4
5
6
7
8
9
10
11
| <% form_for(@chore) do |f| %>
<%= f.error_messages %>
<p>
<%= f.label :description %><br />
<%= f.text_field :description %>
</p>
<%= f.submit "Add" %>
</p>
<% end %> |
Looking back at the functional tests, this seems a little on the light side. For instance the tests passed without ever checking to see what happened with save of Chore. I’m going to go back and add a couple mote tests. Adding the following two tests
1
2
3
4
5
6
7
8
9
| specify "that it should send a message that chore was created" do
post :create, :chore => { :description => 'My Chore' }
assert_equal "Chore added.", flash[:message]
end
specify "that it should send a message that chore creation failed" do
post :create, :chore => { :description => nil }
assert_equal "Error saving Chore.", flash[:error]
end |
changes the create event to be
1
2
3
4
5
6
7
8
9
10
| def create
@chore = Chore.new(params[:chore])
if @chore.save
flash[:message] = "Chore added."
redirect_to chores_url
else
flash[:error] = "Error saving Chore."
render :action => "new"
end
end |
Well that should be enough to move on with our cucumber feature and a solid beginning for our chore controller. Running rake features again shows us that we indeed have passed the next step. It is now looking for a field that isn’t there.
$ cucumber features
Story: Define chores # features/define_chores.feature
As a parent
I want to define chores
So that I can assign them to my children
Scenario: Creating a chore # features/define_chores.feature:6
Given I am on the homepage # features/step_definitions/chores_steps.rb:1
When I follow "Add Chore" # features/step_definitions/webrat_steps.rb:8
And I fill in the chore with "My first chore" # features/step_definitions/chores_steps.rb:5
Could not find field labeled "chore[name]" (RuntimeError)
c:/Ruby/lib/ruby/gems/1.8/gems/webrat-0.3.4/lib/webrat/core/flunk.rb:4:in 'flunk'
c:/Ruby/lib/ruby/gems/1.8/gems/webrat-0.3.4/lib/webrat/core/locators.rb:16:in 'field_labeled'
c:/Ruby/lib/ruby/gems/1.8/gems/webrat-0.3.4/lib/webrat/core/locators.rb:10:in 'field'
c:/Ruby/lib/ruby/gems/1.8/gems/webrat-0.3.4/lib/webrat/core/scope.rb:183:in 'locate_field'
c:/Ruby/lib/ruby/gems/1.8/gems/webrat-0.3.4/lib/webrat/core/scope.rb:41:in 'fills_in'
c:/Ruby/lib/ruby/gems/1.8/gems/webrat-0.3.4/lib/webrat/rails.rb:88:in 'send'
c:/Ruby/lib/ruby/gems/1.8/gems/webrat-0.3.4/lib/webrat/rails.rb:88:in 'method_missing'
c:/Ruby/lib/ruby/gems/1.8/gems/actionpack-2.2.2/lib/action_controller/integration.rb:498:in '__send__'
c:/Ruby/lib/ruby/gems/1.8/gems/actionpack-2.2.2/lib/action_controller/integration.rb:498:in 'method_missing'
./features/step_definitions/chores_steps.rb:6:in 'And /^I fill in the chore with "(.*)"$/'
features/define_chores.feature:9:in `And I fill in the chore with "My first chore"'
And I press Add # features/step_definitions/chores_steps.rb:9
Then I should see "Chore added." # features/step_definitions/webrat_steps.rb:83
And I should see "My first chore" # features/step_definitions/webrat_steps.rb:83
1 scenario
2 steps passed
1 step failed
3 steps skipped
It appears that I had intended to name the field Chore.name, but when it came around time to name the field, I named it Chore.description. I’ll go ahead and modify my step definition to read ‘chore[description]‘ and try again.
Much better this time. Looks like the feature is looking for the flash message to be displayed on screen. Currently we aren’t doing that, so I’ll need to drop that in.
$ cucumber features
Story: Define chores # features/define_chores.feature
As a parent
I want to define chores
So that I can assign them to my children
Scenario: Creating a chore # features/define_chores.feature:6
Given I am on the homepage # features/step_definitions/chores_steps.rb:1
When I follow "Add Chore" # features/step_definitions/webrat_steps.rb:8
And I fill in the chore with "My first chore" # features/step_definitions/chores_steps.rb:5
And I press Add # features/step_definitions/chores_steps.rb:9
Then I should see "Chore added." # features/step_definitions/webrat_steps.rb:83
expected: /Chore added./m,
got: "\n" (using =~)
Diff:
@@ -1,2 +1,2 @@
-/Chore added./m
+"\n" (Spec::Expectations::ExpectationNotMetError)
./features/step_definitions/webrat_steps.rb:84:in `Then /^I should see "(.*)"$/'
features/define_chores.feature:11:in `Then I should see "Chore added."'
And I should see "My first chore" # features/step_definitions/webrat_steps.rb:83
1 scenario
4 steps passed
1 step failed
1 step skipped
Well, this isn’t beautiful but if I add this layout (as app/views/layout/application.html.erb), the flash will display and the feature step will pass and we can move on.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
| <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head>
<meta http-equiv="content-type" content="text/html;charset=UTF-8" />
<title>Chores
</title>
</head>
<body>
<%= flash[:error] %>
<%= flash[:message] %>
<%= yield %>
</body>
</html> |
that passes our next step, and we are onto the step that says ‘And I should see “My first chore”‘. This means that we are expecting it to have redirected us to index and that the new record that we just added should display on screen. Thus far I have placed nothing on the index view, so I will add the following.
1
2
3
| <% @chores.each do |chore| %>
<%=h chore.description %>
<% end %> |
and now cucumber gives us some of it’s green cucumbery goodness with a passing test.
$ cucumber features
Story: Define chores # features/define_chores.feature
As a parent
I want to define chores
So that I can assign them to my children
Scenario: Creating a chore # features/define_chores.feature:6
Given I am on the homepage # features/step_definitions/chores_steps.rb:1
When I follow "Add Chore" # features/step_definitions/webrat_steps.rb:8
And I fill in the chore with "My first chore" # features/step_definitions/chores_steps.rb:5
And I press Add # features/step_definitions/chores_steps.rb:9
Then I should see "Chore added." # features/step_definitions/webrat_steps.rb:83
And I should see "My first chore" # features/step_definitions/webrat_steps.rb:83
1 scenario
6 steps passed
Now I’ll commit to git and script/server to take a look at what our testing has created. It looks good, once I remove index.html from the public directory, I can see the Add Chore link from the home page. Clicking on it gives me a validated form that allows me to enter a chore entry and save it. We also have tests that tell us that it’s all working well and functioning as expected.
We have taken a look at implementing a feature with Test::Unit, webrat and cucumber and it’s really not all that difficult to do. Really the process from this point on is rinse, repeat, refactor. I’m not sure at this point if I’ll return to post on Chores or not. It’s been interesting documenting my process as I go along, but it’s also been a weight slowing me down. In any case, I hope you have enjoyed this series on test-driven development and maybe have decided to try it out for yourself. I hope so, and if you have kids, maybe check out chores when it’s done.