Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP

Building a Sinatra App Driven By Webrat Tests

cowmanifestation edited this page · 38 revisions

For this tutorial on Webrat testing, I will be demonstrating step-by-step the creation of a Sinatra app, using the technique of first specifying the desired behavior and writing Webrat tests to keep my code on the straight and narrow path of meeting those requirements. Note that I am also using Sinatra, PStore and ERB here, but this is not a tutorial for them, so I haven't explained what I'm doing with them as extensively as I have with Webrat. If you need clarification with anything, Sinatra, PStore or ERB-related, please feel free to get in touch with me. For your convenience, the Sinatra documentation is here; that for PStore is here and for ERB, here.

The app I've chosen to create is a very simple cookbook, where you can enter recipes, view a list of all the recipes in alphabetical order, and by clicking on a recipe title, view the full body of the recipe. I plan to use PStore, a simple library for storing data in a hash-like format, to store the recipes; and ERB, a library which gives us the capability of inserting Ruby code into HTML, to create the actual content of each page.

I will be writing this in a test-driven fashion -- in other words, creating the tests first, running them to ensure that they fail, and then writing the code to make them pass. An important aspect of creating this program is also running and playing with it in my browser to make sure that it looks and feels the way I want it to.

You can see segments of code as I write them here, but if you want to see the entire, completed program, it can be found in this code repository.

I'm writing this using Ruby version 1.8.7, Sinatra 1.0, and Webrat 0.7.1.

With that, let's begin.

Webrat setup and initial test

First, I will be writing my Webrat test code, which I will put in a file named test.rb.

require 'rubygems'
require 'rack/test'
require 'webrat'
require 'test/unit'
require 'contest'
require 'app'

Webrat.configure do |config|
  config.mode = :rack
end

class AppTest < Test::Unit::TestCase
  include Rack::Test::Methods
  include Webrat::Methods
  include Webrat::Matchers

  def app
    Sinatra::Application.new
  end

  test "can access home page" do
    visit "/"
    assert_contain("Welcome to the Cookbook!")
  end
end

Run this, and watch it fail: it will complain about the lack of a file entitled "app" to load.

Now, in a file (within the same folder) which I simply call app.rb, I compose the first Sinatra step:

require 'rubygems'
require 'sinatra'

get "/" do
  "Welcome to the Cookbook!"
end

Run the test again, and it will pass.

Form Creation and Testing

Next, in Webrat - ensure that submitting takes us to the page which will contain an index of all of the recipes; and, for now, that both recipe title and body are present on that page.

  test "form entry" do
    title = "Apple Pie"
    recipe = "Put some apples in a crust and bake!"

    visit "/entry"
    fill_in "title", :with => title
    fill_in "recipe", :with => recipe
    click_button "Submit"
    assert_contain(title + ": " + recipe)
  end

When the test is run, this is the result:

$ ruby test.rb
Loaded suite test
Started
E.
Finished in 0.03125 seconds.

  1) Error:
test_form_entry(AppTest):
Webrat::NotFoundError: Could not find field: "title"
    c:/Ruby187/lib/ruby/gems/1.8/gems/webrat-0.7.1/lib/webrat/core/locators/loca
tor.rb:14:in `locate!'
    c:/Ruby187/lib/ruby/gems/1.8/gems/webrat-0.7.1/lib/webrat/core/locators/fiel
d_locator.rb:21:in `field'
    c:/Ruby187/lib/ruby/gems/1.8/gems/webrat-0.7.1/lib/webrat/core/scope.rb:343:
in `locate_field'
    c:/Ruby187/lib/ruby/gems/1.8/gems/webrat-0.7.1/lib/webrat/core/scope.rb:51:i
n `fill_in'
    c:/Ruby187/lib/ruby/gems/1.8/gems/webrat-0.7.1/lib/webrat/core/methods.rb:7:
in `fill_in'
    test.rb:27:in `test_form_entry'

2 tests, 1 assertions, 0 failures, 1 errors

This test (and the very first one that I wrote, which you hopefully ran) demonstrates something very useful about Webrat: Not only the assert____ methods (which come from Test::Unit) are tests, but the other methods such as visit "/", fill_in("title") and click_button "Submit" are tests, and will serve up an informative error when they don't work.

Next, we add this to our Sinatra file, app.rb:

get "/entry" do
  erb :entry
end

However, when run, we get the same error as above, because we have yet to build our erb file for this page! To do this, we create a folder entitled "views" within the folder containing our app and tests, and within that, create a file entry.erb, and into that file, put this:

<html>
  <body>
    <form method="post" action ="/">
      <p>Title:</p>
      <input type="text" name="title" size = "50">
      <p>Recipe:</p>
      <textarea rows = "25" cols = "75" wrap = "virtual" name = "recipe"></textarea>
      <input type="submit" value="Submit">
    </form>
  </body>
</html>

Now the result of running our tests is this:

$ ruby test.rb
Loaded suite test
Started
F.
Finished in 0.046875 seconds.

  1) Failure:
test_form_entry(AppTest)
    [c:/Ruby187/lib/ruby/gems/1.8/gems/webrat-0.7.1/lib/webrat/core/matchers/hav
e_content.rb:57:in `assert_contain'
     test.rb:32:in `test_form_entry']:
expected the following element's content to include "Apple Pie:Put some apples i
n a crust and bake!":

  body { text-align:center;font-family:helvetica,arial;font-size:22px;
    color:#888;margin:20px}
  #c {margin:0 auto;width:500px;text-align:left}
  Sinatra doesn't know this ditty.
    Try this:
    post '/' do
  "Hello World"
end
.
<false> is not true.

2 tests, 2 assertions, 1 failures, 0 errors

This failure, as Webrat tells us, is due to a lack of a defined post "/" ... method in our Sinatra app, which we will now add.

post "/" do
  recipe = params[:recipe]
  title = params[:title]
  "#{title}:#{recipe}"
end

Run it, and we see passing tests:

$ ruby test.rb
Loaded suite test
Started
..
Finished in 0.03125 seconds.

2 tests, 2 assertions, 0 failures, 0 errors

Introduction of Storage Capability

Now, I want to be able to enter a recipe, go back and enter another recipe, and come to the list page and see both of them there. Here's my initial test for this:

  test "first recipe still exists after adding second" do
    ["A", "C", "B", "Boo"].each do |e|
      visit "/entry"
      fill_in "title", :with => e
      fill_in "recipe", :with => "recipe"
      click_button "Submit"
    end
    assert_contain("A: recipe")
    assert_contain("B: recipe")
    assert_contain("C: recipe")
    assert_contain("Boo: recipe")
  end

Of course, with our existing program, this is what we get:

$ ruby test.rb
Loaded suite test
Started
F..
Finished in 0.0625 seconds.

  1) Failure:
test_first_recipe_still_exists_after_adding_second(AppTest)
    [c:/Ruby187/lib/ruby/gems/1.8/gems/webrat-0.7.1/lib/webrat/core/matchers/hav
e_content.rb:57:in `assert_contain'
     test.rb:46:in `test_first_recipe_still_exists_after_adding_second']:
expected the following element's content to include "A:recipe":
Boo:recipe.
<false> is not true.

3 tests, 3 assertions, 1 failures, 0 errors

In order to accomplish storage of previously entered recipes, I'm going to add the functionality of PStore, which, as I mentioned earlier, is a file format in which one can store data in a Hash-like format. First, we'll create a helpers section within our app.rb, placed above the get "/" method, and in that, a method that initializes and assigns a variable to a PStore file (make sure that, at the top of the file, you require pstore). We'll also add to this the functionality of specifying the name of the PStore file in the command line when the app is run, so that we can use different files for, say, testing and production.

helpers do
  def store
    @store ||= ARGV[0]? PStore.new(ARGV[0]) : PStore.new("recipes.store")
  end
end

In our app.rb, we'll set our post "/" method to also store any newly created recipe in our PStore file, as well as call upon an ERB file which displays our recipes, so that we can create some HTML formatting.

post "/" do
  @recipe = params[:recipe]
  @title = params[:title]
  if @title
    store.transaction { store[@title] = @recipe }
  end
  erb :recipes
end

Now we can also change our get "/" method to also call upon recipes.erb:

get "/" do
  erb :recipes
end

And in our views folder, we create recipes.erb, making sure to include the welcome message that was previously in the get "/" method in order to avoid offending our very first test:

<html>
  <head>
    <h1>Welcome to the Cookbook!</h1>
  </head>
  <body>
    <p><%=@title + ": " + @recipe %></p>
  </body>
</html>

Now try running the tests. As you can see, they still don't pass.

When I look at my directory, I find that there is, indeed, a .store file in my directory now - my app successfully created it. This is a good step towards achieving the functionality that we seek, but there are still several steps to go.

We still have to get our recipes.erb to display not just one, but all recipes in our PStore file.

(Note: Don't forget to run the Sinatra app and test it in your browser, to see how it looks!)

I add a method to my app.rb which calls up each recipe - the roots of the PStore - and sorts them:

  def recipe_list
    recipes = store.transaction { store.roots }
    recipes.sort
  end

I revise our recipes.erb to list each recipe provided by the recipe_list method:

<html>
  <head>
    <h1>Welcome to the Cookbook!</h1>
  </head>
  <body>
    <ul>
      <% recipe_list.each do |r| %>
        <li><%= r %>: <%= store[r] %></li>
      <% end %>
    </ul>
  </body>
</html>

Now my tests pass. I'd like to test that the recipes are listed in alphabetical order - which, I can see from looking at the page in my browser, they are - so I'll have to figure out how to do that.

Testing Alphabetization

And after some exploration of the Webrat documentation, I discover that it has methods such as have_xpath and have_tag, which might be helpful in this situation. Unsure of how to test the order of tags, I delve further into Xpath information - this Xpath guide was very helpful - and discover that nodes are ordered, and therefore alphabetical order can be tested in this fashion:

test "recipe list is alphabetized" do
  {"Cherry Pie" => "Is the Best", "Apple Pie" => "Is only good when Lynn makes it.", 
   "Beans" => "Are good with rice."}.each do |title, recipe|
    visit "/entry"
    fill_in "title", :with => title
    fill_in "recipe", :with => recipe
    click_button "Submit"
  end

  assert_have_xpath "//ul/li[1][a='Apple Pie']"
  assert_have_xpath "//ul/li[2][a='Beans']"
  assert_have_xpath "//ul/li[3][a='Cherry Pie']"
end

This test fails. Are the recipes not in alphabetical order? Start up the app and look at it in your browser - you'll see that they are. But there is another problem. Perhaps you see what it is. Try and find it if you haven't already.

NOTE: The XPath syntax that I've used doesn't work for everyone. You may have to try this instead: assert_have_xpath "//ul/li[1][contains(.,'Apple Pie')]", etc. If, after making the changes that I am about to suggest, your test still doesn't pass, try changing the syntax and see if that fixes the problem.

Do you see how many recipes are on the list page? Every recipe from every test ever run, through Webrat or manually, is there. Sure, they are in alphabetical order - but the list obviously isn't what our most recent test expects. Rather than trying to figure out which recipes exist from all the previous tests and altering this test to fit them, and perhaps having to change it again if we create a new test which might be run before this, wouldn't it be easier to somehow ensure that we could have a clean slate before this test? Perhaps we might even find that such functionality is useful for other tests as well.

Setup and Teardown

Luckily for us, there already are convenient structures that we can use to ensure a clean slate before each test. They are called setup and teardown, and all we have to do is define them. We can also deal with another potential problem here - our tests are messing with the same PStore file that is used by our app in general! To handle that, I simply specify a filename for our app's ARGV[0] in setup, as well as creating the file that the test will be using. In teardown, I delete the file. Thus it will be newly created and destroyed for each test.

setup do
  ARGV[0] = "test.store"
  file = File.new("test.store", "a+")
  file.close
end

teardown do
  File.delete("test.store")
end

Giving Each Recipe Its Own Page

Next on my agenda is to move the body of the recipes each to its own page, which, you might be able to image, will look a lot nicer, especially when we are entering real, lengthy recipes. I plan on keeping only the titles as links on the list page.

Test:

  test "link from list to individual recipe page" do
    visit "/entry"
    fill_in "title", :with => "Tomato Sauce"
    fill_in "recipe", :with => "Cook some tomatoes in a sauce pan."
    click_button "Submit"

    click_link "Tomato Sauce"
    assert_contain "Cook some tomatoes in a sauce pan."
  end

Failure:

Started
....E.
Finished in 0.09375 seconds.

  1) Error:
test_link_from_list_to_individual_recipe_page(AppTest):
Webrat::NotFoundError: Could not find link with text or title or id "Tomato Sauce"

    c:/RUBY187/lib/ruby/gems/1.8/gems/webrat-0.7.2/lib/webrat/core/locators/loca
tor.rb:14:in `locate!'
    c:/RUBY187/lib/ruby/gems/1.8/gems/webrat-0.7.2/lib/webrat/core/locators/link
_locator.rb:78:in `find_link'
    c:/RUBY187/lib/ruby/gems/1.8/gems/webrat-0.7.2/lib/webrat/core/scope.rb:276:
in `click_link'
    c:/RUBY187/lib/ruby/gems/1.8/gems/webrat-0.7.2/lib/webrat/core/methods.rb:7:
in `click_link'
    test.rb:86:in `test_link_from_list_to_individual_recipe_page'

Indeed, there is no link. First we're going to create this method in app.rb in the helpers to convert the title of the recipe to proper URI format, along with a companion method to change this part of the URI back into a title so that we can use it to key into our PStore. For this we will be using Ruby's standard OpenURI library, so don't forget to require open-uri at the top of app.rb.

  def to_uri(str)
    URI.encode(str)
  end

  def to_title(str)
    URI.decode(str)
  end

To turn titles of recipes into links, I change the line in the midst of recipes.erb, below <body> to this:

   <ul>
      <% recipe_list.each do |r| %>
        <li><a href="/<%= to_uri(r) %>"><%= r %></a>: <%= store[r] %></li>
      <% end %>
    </ul>

Now running the test brings us this error:

Loaded suite test
Started
....F.
Finished in 0.140625 seconds.

  1) Failure:
test_link_from_list_to_individual_recipe_page(AppTest)
    [c:/Ruby187/lib/ruby/gems/1.8/gems/webrat-0.7.1/lib/webrat/core/matchers/hav
e_content.rb:57:in `assert_contain'
     test.rb:77:in `test_link_from_list_to_recipe_page']:
expected the following element's content to include "Cook some tomatoes in a sauce pan.":

  body { text-align:center;font-family:helvetica,arial;font-size:22px;
    color:#888;margin:20px}
  #c {margin:0 auto;width:500px;text-align:left}
  Sinatra doesn't know this ditty.
    Try this:
    get '/Tomato-Sauce' do
  "Hello World"
end
.
<false> is not true.

6 tests, 9 assertions, 1 failures, 0 errors

This is because we haven't created a get ... method for this link, nor an erb file for the page, yet. In app.rb:

get "/:recipe" do
  @recipe = params[:recipe]
  @title    = to_title(@recipe)
  erb :recipe
end

I'm also going to write a retrieve_recipe method in the helpers that we can use to show the body of the recipe:

  def retrieve_recipe(recipe)
    store.transaction { store[recipe] }
  end

New file - views/recipe.erb:

<html>
  <% @title %>
  <head>
    <h1><%= "#{@recipe}" %></h1>
  </head>
  <body>
    <p><%= "#{retrieve_recipe(@title)}" %></p>
  </body>
</html>

The tests pass.

Now I'm going to remove the body of the recipe from the list page, so that it can only be seen on its own page. In recipes.erb, the same segment we recently inserted:

    <ul>
      <% recipe_list.each do |r| %>
        <li><a href="/<%= to_uri(r) %>"><%= r %></a></li>
      <% end %>
    </ul>

When I start up the app and play with it in my browser, it works as I had desired. But the tests fail!

  1) Failure:
test_first_recipe_still_exists_after_adding_second(AppTest)
    [c:/Ruby187/lib/ruby/gems/1.8/gems/webrat-0.7.1/lib/webrat/core/matchers/hav
e_content.rb:57:in `assert_contain'
     test.rb:53:in `test_first_recipe_still_exists_after_adding_second']:
expected the following element's content to include "A: recipe":

    A
          B
          Boo
          C
    enter
.
<false> is not true.

  2) Failure:
test_form_entry(AppTest)
    [c:/Ruby187/lib/ruby/gems/1.8/gems/webrat-0.7.1/lib/webrat/core/matchers/hav
e_content.rb:57:in `assert_contain'
     test.rb:43:in `test_form_entry']:
expected the following element's content to include "Apple Pie: Put some apples
in a crust and bake!":

    Apple Pie
    enter
.
<false> is not true.

6 tests, 6 assertions, 2 failures, 0 errors

This is because some of the tests that we previously wrote look for the body of the recipe, along with the title, on the page containing the list of recipes. Before reading further, scroll back up, or if you have been following this with the creation of your own code, take a look at your equivalent to test.rb and see if you can identify those tests.

Now, we're going to change them to fit our current version of the app. In test "form entry", change the line assert_contain(title + ": " + recipe) to assert_contain(title). And in test "first recipe still exists after adding second", change the line assert_contain("A: recipe") and the three subsequent, similar lines to assert_contain("A") or the equivalent.

Our tests now pass.

Additional Conveniences

What more could we possibly want? Well, there are a couple more things that would make this app less annoying. One is a link from the list page to the recipe entry form; another is a link from a recipe page to the list of recipes. We could also add a link from the entry form back to the recipe page, perhaps a "Cancel" button.

To create a link from the list to the entry page, a test:

  test "link from list to recipe creation form" do
    visit "/"
    click_link("enter a new recipe")

    assert_contain("Title")
    assert_contain("Recipe")
  end

Which fails, of course:

  1) Error:
test_link_from_list_to_recipe_creation_form(AppTest):
Webrat::NotFoundError: Could not find link with text or title or id "enter a new
 recipe"
    c:/Ruby187/lib/ruby/gems/1.8/gems/webrat-0.7.1/lib/webrat/core/locators/loca
tor.rb:14:in `locate!'
    c:/Ruby187/lib/ruby/gems/1.8/gems/webrat-0.7.1/lib/webrat/core/locators/link
_locator.rb:70:in `find_link'
    c:/Ruby187/lib/ruby/gems/1.8/gems/webrat-0.7.1/lib/webrat/core/scope.rb:276:
in `click_link'
    c:/Ruby187/lib/ruby/gems/1.8/gems/webrat-0.7.1/lib/webrat/core/methods.rb:7:
in `click_link'
    test.rb:60:in `test_link_from_list_to_entry'

4 tests, 5 assertions, 0 failures, 1 errors

To rectify this in our code, I add this line to the bottom of our recipes.erb, before the closing </body> and </html> tags of course:

    <a href="/entry">enter a new recipe</a>

And the test passes.

Refactorization

At this point, I am about to write a fourth test that fills out the recipe entry form and submits it, so first I'm going to extract that functionality to its own method at the top of our test.rb:

  def create_and_submit_recipe(title="Cherry Pie", recipe="Is the Best!")
    visit "/entry"
    fill_in "title", :with => title
    fill_in "recipe", :with => recipe
    click_button "Submit"
  end

Now we can replace four lines in three of our existing tests with a call to this method, with or without arguments, as needed. I'll leave it to you to figure out what they are. If you're following along and are not sure how to do this, try following the example in the test that I'm about to write.

To test a link from the individual recipe page to the list of recipes:

  test "link from individual recipe to list page" do
    create_and_submit_recipe

    click_link "Cherry Pie"
    click_link "Back to Recipes"

    assert_contain("enter")
  end

Of course it fails - the error message includes Webrat::NotFoundError: Could not find link with text or title or id "Back to Recipes" ... and here's the remedy, to insert in our recipe.erb beneath the paragraph containing the body of the recipe:

<a href= "/">Back to Recipes</a>

The tests pass. Next, I'd like to be able to easily cancel the entry of a recipe, so I will insert a "Cancel" link on the entry page. We've created several links already, and created plenty of tests, so you should have the tools to do this on your own. If you have problems, you can find the completed solution in the code repository for this project.

And here we will finish. This app is, arguably, incomplete, and by no means is it pretty -- but I hope that this demonstration has provided you with enough information to confidently begin testing your Sinatra apps with Webrat. Feel free to contact me at cowmanifestation@gmail.com with any questions or comments!

Something went wrong with that request. Please try again.