====== simple example app ====== [[http://merbist.com|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 [[http://github.com/mattetti/simple_merb_example_app/tree/master|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 ==== [[https://gist.github.com/4d1bc4715d3be7e3dff6|diff file]] [[http://github.com/mattetti/simple_merb_example_app/tree/step3_generate_article_resource|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) [[https://gist.github.com/4a7ee1eb20d220da9940|Diff file]] [[http://github.com/mattetti/simple_merb_example_app/tree/step6_add_model_specs|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 ==== [[https://gist.github.com/fd777615f4278d5cd3f8|Diff file]] [[http://github.com/mattetti/simple_merb_example_app/tree/step5_add_model_validation|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 ==== [[https://gist.github.com/716269a0eebe0eed9bba|Diff file]] [[http://github.com/mattetti/simple_merb_example_app/tree/step7_edit_request_specs|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 ==== [[https://gist.github.com/116ad424ead4ba52ae50|Diff file]] [[http://github.com/mattetti/simple_merb_example_app/tree/step9_make_specs_not_pending|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 [[SimpleAppStep9Fix|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 [[http://github.com/brynary/webrat|webrat]] helpers. ==== step 8 edit index view ==== [[https://gist.github.com/b36382433776f5a85e51|Diff file]] [[http://github.com/mattetti/simple_merb_example_app/tree/step8_edit_index_view|Source files]] Let's edit the view to make the request spec pass. # /app/views/articles/index.html.erb ==== Step 10 edit the layout ==== [[https://gist.github.com/3d69e5849d85d8958606|Diff file]] [[http://github.com/mattetti/simple_merb_example_app/tree/step10_edit_layout|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 ==== [[https://gist.github.com/8767219938ab0cd0ed48|Diff file]] [[http://github.com/mattetti/simple_merb_example_app/tree/step11_authenticate_articles_route|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 ==== [[https://gist.github.com/922a449ca87b1e3091d1|Diff file]] [[http://github.com/mattetti/simple_merb_example_app/tree/step13_run_app_specs | 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 [[http://datamapper.org/doku.php?id=docs:validations|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: #> 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 [[/testing/rspec/datamapper-transactions|Check this example to setup DM transactions in your tests]]