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:
$ 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
$ 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
$ 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.
(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
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)
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' }})
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.
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>
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 :)
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.
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.
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.
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
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