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