simple example app

Mattetti has a repo with an example app. The app was auto generated by a script and each step is wrapped in a git branch.

This example covers:

  • generating an application
  • generating a resource
  • specing/testing a model
  • adding model validation
  • specing/testing requests
  • modifying views and layouts
  • passing flash messages (messages passed from one action to another)
  • adding authenticated routes
  • testing authenticated requests

source code repository

Steps:

steps 1 & 2 clean up and generate an app called: my-first-app

$ merb-gen app my-first-app
Generating with app generator:
     [ADDED]  tasks/merb.thor
     [ADDED]  .gitignore
     [ADDED]  public/.htaccess
     [ADDED]  tasks/doc.thor
     [ADDED]  public/javascripts/jquery.js
     [ADDED]  doc/rdoc/generators/merb_generator.rb
     [ADDED]  doc/rdoc/generators/template/merb/api_grease.js
     [ADDED]  doc/rdoc/generators/template/merb/index.html.erb
     [ADDED]  doc/rdoc/generators/template/merb/merb.css
     [ADDED]  doc/rdoc/generators/template/merb/merb.rb
     [ADDED]  doc/rdoc/generators/template/merb/merb_doc_styles.css
     [ADDED]  doc/rdoc/generators/template/merb/prototype.js
     [ADDED]  public/favicon.ico
     [ADDED]  public/images/merb.jpg
     [ADDED]  public/merb.fcgi
     [ADDED]  public/robots.txt
     [ADDED]  Rakefile
     [ADDED]  app/controllers/application.rb
     [ADDED]  app/controllers/exceptions.rb
     [ADDED]  app/helpers/global_helpers.rb
     [ADDED]  app/models/user.rb
     [ADDED]  app/views/exceptions/not_acceptable.html.erb
     [ADDED]  app/views/exceptions/not_found.html.erb
     [ADDED]  autotest/discover.rb
     [ADDED]  autotest/merb.rb
     [ADDED]  autotest/merb_rspec.rb
     [ADDED]  config/database.yml
     [ADDED]  config/dependencies.rb
     [ADDED]  config/environments/development.rb
     [ADDED]  config/environments/production.rb
     [ADDED]  config/environments/rake.rb
     [ADDED]  config/environments/staging.rb
     [ADDED]  config/environments/test.rb
     [ADDED]  config/init.rb
     [ADDED]  config/rack.rb
     [ADDED]  config/router.rb
     [ADDED]  public/javascripts/application.js
     [ADDED]  public/stylesheets/master.css
     [ADDED]  merb/merb-auth/setup.rb
     [ADDED]  merb/merb-auth/strategies.rb

step 3 generate an article resource

diff file Source files

$ merb-gen resource article title:string,author:string,created_at:datetime
     [ADDED]  spec/models/article_spec.rb
     [ADDED]  app/models/article.rb
     [ADDED]  spec/requests/articles_spec.rb
     [ADDED]  app/controllers/articles.rb
     [ADDED]  app/views/articles/index.html.erb
     [ADDED]  app/views/articles/show.html.erb
     [ADDED]  app/views/articles/edit.html.erb
     [ADDED]  app/views/articles/new.html.erb
     [ADDED]  app/helpers/articles_helper.rb
resources :articles route added to config/router.rb

step 4 automigrate the database

$ rake db:automigrate 

Since we are at it, if you are not using Merb 1.0.5 or newer, let's automigrate the test db too:

$ rake db:automigrate MERB_ENV=test

You will more than likely want to edit your spec_helper.rb file to automigrate before running all the specs, check step 15 to see how to do that. Or look at a newly generated application's spec/spec_helper.rb file.

step 6 add model specs

(in real life, this is step 5, but the source branch is named step 6) Diff file Source files

# spec/models/article_spec.rb
it "should not be valid without a title" do
  article = Article.new
  article.should_not be_valid
end

Your model specs should fail.

Note: to run all your specs, use the following command:

 $ rake spec

Or

 $ rake spec:model

To only run the model specs.

You can see all the available rake tasks by doing:

  $ rake -T

step 5 add title validation to Article

Diff file Source files

Add require 'dm-validations' before the class definition

Add the following line to app/models/article.rb

 validates_present :title

(the model specs should now pass)

step 7 edit the request specs

Diff file Source files

Run requests specs:

$ rake spec:request

The requests specs are failing because test database wasn't created and the article params passed are not valid, let's fix that.

Create test database:

$ rake db:automigrate MERB_ENV=test

Change the params sent to create a new article:

# /spec/requests/articles_spec.rb
# replace :params => { :article => { :id => nil }})   by
:params => { :article => {:title => 'intro', :author => 'Matt', :created_at => '2008-11-16 19:33:13' }})

step 9 remove the pending spec

Diff file Source files

Once again, in real life, you would more than likely use TDD and therefore write this spec first, however because the script doesn't run in this order, I kept the name of the git branch. (step 9)

By default Merb generate requests some pending request specs since it doesn't know what you are going to do with your views.

# after removing the pending calls, your code will look something like:
    it "has a list of articles" do
      @response.should have_xpath("//ul/li")
    end

(if you are getting “NoMethodError […] has_xpath?” here, please try this)

Note that you don't have to use xpath, you can use one of the other views matchers such as:

  @response.should have_selector("ul > li")
  @response.should have_selector("ul > li > label")
  @response.should have_tag(:label)
  # check that the article title is being displayed
  @response.should contain("intro")

You can also use any of the webrat helpers.

step 8 edit index view

Diff file Source files

Let's edit the view to make the request spec pass.

# /app/views/articles/index.html.erb
<ul>
  <% @articles.each do |article| %>
    <li><label>Title:</label><%= article.title %></li>
  <% end %>
</ul>

Step 10 edit the layout

Diff file Source files

For the specs to pass we need to display the flash messages. As you can see, the following spec checks that an error message is being displayed:

 
  describe "a failing POST" do
    before(:each) do
      @response = request(resource(:articles), :method => "POST", :params => { :article => { :id => nil}})
    end
 
    it "should have an error message" do
      @response.body.should include("Article failed to be created")
    end
  end

To make this spec pass, we need to edit the layout and make sure to display the error and notice messages.

# app/views/layout/application.html.erb
   <%= message[:notice] %>
   <%= message[:error] %>

Now all the specs should pass :)

Step 11 Authenticate a route

Diff file Source files

Let's authenticate a route using the router (you might want to do that in the controller, check the authentication documentation for more details).

# config/router.rb
  authenticate do
    resources :articles  
  end

This will protect the articles resource routes and will make sure users who want to access the resource are logged in.

Step 12 spec the authenticated routes

Diff file Source files

Now that we added authentication, all our request specs will fail because we are not logged in. Fear not, we'll fix that quickly. Start by editing the spec/spec_helper.rb and add 2 helpers:

Merb::Test.add_helpers do
 
  def create_default_user
    unless User.first(:login => "krusty")
      User.create(:login => "krusty", 
                  :password => "klown", 
                  :password_confirmation => "klown") or raise "can't create user"
    end
  end
 
  def login
    create_default_user
    request("/login", {
      :method => "PUT",
      :params => {
        :login => "krusty",
        :password => "klown"
      }
    })
  end
 
end

The first helper (#create_default_user) creates a user unless it already exists, the second helper (#login) sends a login request to merb-auth with the login and password of the default user. Note that before making the request, we check that the default exists. Also, note that the http verb to use to login is PUT, not POST.

Now that we have this helper, we can go to each “before block” and call the login method as follows:

     before(:each) do
       login
       @response = request(resource(:articles))
     end

All the specs should now pass again. You might want to make sure that if the login failed or a user isn't logged in, the user would be redirected to the login page.

Also, note that when you will add a controller to create a new user (signup), you should probably change the #create_default_user to go through the controller and signup the user instead of using the database directly.

Step 13 generate a comment

Let's add a comment resource so our private blog can have comments.

$ merb-gen resource comment body:text,author:string
     [ADDED]  spec/models/comment_spec.rb
     [ADDED]  app/models/comment.rb
     [ADDED]  spec/requests/comments_spec.rb
     [ADDED]  app/controllers/comments.rb
     [ADDED]  app/views/comments/index.html.erb
     [ADDED]  app/views/comments/show.html.erb
     [ADDED]  app/views/comments/edit.html.erb
     [ADDED]  app/views/comments/new.html.erb
     [ADDED]  app/helpers/comments_helper.rb
resources :comments route added to config/router.rb

Notice that I didn't declare an article id, this will happen automatically when I will declare my relationship in the model.

Step 14 - add comments

Write some specs to make sure the Comment model is working as expected:

#/spec/models/comment_spec.rb
describe Comment do
 
  it "should not be valid without a body" do
    comment = Comment.new
    comment.should_not be_valid
    comment.errors.on(:body).first.should == "Body must not be blank"
  end
 
end

To run the spec for only this controller:

rake spec:request REQUEST=comments

To make this spec pass, we need to edit our Comment model as follows:

 
class Comment
  include DataMapper::Resource
 
  property :id, Serial
  property :body, Text, :nullable => false
  property :author, String
end

By making the body attribute not nullable, it adds an auto validation on the attribute. You can read more about validation on DataMapper's wiki.

Let's refactor our specs and add some more:

describe Comment do
 
  before(:each) do
    @comment = Comment.new(:body => "Ze Body", :author => "Ze Author")
    @comment.should be_valid
  end
 
  it "should not be valid without a body" do
    @comment.body = nil
    @comment.should_not be_valid
    @comment.errors.on(:body).first.should == "Body must not be blank"
  end
 
  it "should not be valid without an author" do
    @comment.author = nil
    @comment.should_not be_valid
    @comment.errors.should_not be_empty
    @comment.errors.on(:author).first.should == "Author must not be blank"
  end
 
  it "should not be valid without an author's name shorter than 3 characters" do
    @comment.author = "ok"
    @comment.should_not be_valid
    @comment.errors.should_not be_empty
    @comment.errors.on(:author).first.should == "Author must be between 3 and 255 characters long"
  end
 
end

We optimized our specs by avoiding repetition and setting up a valid instance variable (starting by @) that can be accessed from each spec. The instance variable (@comment) gets initialized before each spec. We added 2 new specs, one to make sure we have an author and the other to make sure the author's name is at least 3 characters long.

Note: to check if an object has some validation errors, you first need to check if it's valid.

To get the new specs to pass, let's edit the model:

class Comment
  include DataMapper::Resource
 
  property :id, Serial
  property :body, Text, :nullable => false
  property :author, String, :nullable => false, :length => (3..255)
 
end

Let's now make sure we have a relationship between the comment and the article. For this spec, we will just check that the article method is defined on the comment object. This is not a very good test since it doesn't really test the behavior.

  it "should have a relationship with an Article" do
    lambda{ @comment.article }.should_not raise_error
  end

When you will run your spec, you should see an exception like that:

  #<NoMethodError: undefined method `article' for #<Comment id=nil body="Ze Body" author="Ze Author">>

So, let's go ahead and define the relationship:

 belong_to :article

Here is how your model should look like:

class Comment
  include DataMapper::Resource
 
  property :id, Serial
  property :body, Text, :nullable => false
  property :author, String, :length => (3..255), :nullable => false
 
  # Relationships
  belongs_to :article
 
end

Since we are at it, let's add a spec to make sure we have a relationship between an Article object and a comment:

#app/models/article.rb
 
class Article
  include DataMapper::Resource
 
  property :id, Serial
  property :title, String
  property :author, String
  property :created_at, DateTime
 
  # Validation
  validates_present :title
 
  # Relationships
  has n, :comments
end

Step 15 Request specs / Views / Routes

Now that we took care of the model, let's look at how this new model integrates in our app.

By default, the generated route isn't nested and if you look at our router.rb file you'll notice that it's not in our authenticated block. Let's fix that:

# /config/router.rb
    authenticate do
    resources :articles  do
      resources :comments
    end
  end

At this point, if you have a merb app using merb 1.0.4 or older, if you would run your request specs (rake spec:request), they would fail painfully. First thing first, we should upgrade our DB. let's use:

rake db:automigrate MERB_ENV=test

Note that this is a destructive command and you should only use it in development/test or when setting up a new server.

However, this is kind of annoying, if every time I edit my schema, I have to run a rake task, that might drive me crazy really quickly.

Let's modify our spec/spec_helper.rb file to automigrate our database before running our specs.

Spec::Runner.configure do |config|
  config.include(Merb::Test::ViewHelper|>)
  config.include(Merb::Test::RouteHelper|>)
  config.include(Merb::Test::ControllerHelper|>)
 
  config.before(:all) do
    DataMapper.auto_migrate!
  end
end

Check this example to setup DM transactions in your tests

 
example_apps/simple_app.txt · Last modified: 2009/11/19 13:43 by hipe