Popular Posts

Показаны сообщения с ярлыком rspec. Показать все сообщения
Показаны сообщения с ярлыком rspec. Показать все сообщения

четверг, 15 марта 2012 г.

Devise + Cancan. Часть три

Привет.
Продолжим с devise и cancan.
Что будем делать?
Допустим я хочу сделать так чтобы даже не вошедший пользователь мог бы сделать пост, нет
 он его не опубликует, просто его перекинет на страницу входа. Вот.
Для этого делаем контроллер таким:
#controllers/post_controller.rb
class PostsController < ApplicationController
 
  before_filter :authenticate_user!, :only => [:new]
  load_and_authorize_resource
  skip_authorize_resource :only => :new 
  #...
end
Как видно тут добавлен before_filter и метод :authenticate_user!, который только для действия new.
Метод :authenticate_user! он определён в devise.
Что он делает?
Если пользователь вошёл, то ничего (на самом деле он проверяет)
Если не произведён вход на сайт, то будет перенаправлен (redirect_to) на страницу входа /sign_in
Далее стандартная load_and_authorize_resource из cancan
Затем снова метод из cancan skip_authorize_resource :only => :new который сбрасывает проверку привилегий для
метода (:new).
Вот достаточно просто.
Ах да ещё убираем из view нашу проверку
#views/posts/index.html.erb
<%= link_to 'New Post', new_post_path %>
убрали can? перед ссылкой на создание нового поста.

Что дальше?
Для системы, которую делаю, неплохо бы добавить к постам комментарии, это же оживляет страницу)
Создаём модель\контроллер\и сразу миграцию
rails g model comment post:references user:references body:text
rails g controller comments
rake db:migrate
Хоп! Всё сделано, остался код + тесты
В класс модели User добавляем след. строчку:
#models/user.rb
class User < ActiveRecord::Base
# ...
has_many :comments, :dependent => :destroy
end
Этот макрос отношение один-ко-многим, то есть у одного пользователя может быть сотня-другая комментраиев.
В модель Post
#models/post.rb
class Post < ActiveRecord::Base
#...
has_many :comments, :dependent => :destroy
end
То же что и для User у одного поста много-много комментариев, с удалением поста все они тоже будут удалены.
Ну и далее нужная нам новая модель Comment
#models/comment.rb
class Comment < ActiveRecord::Base
  belongs_to :post
  belongs_to :user
  validates :user_id, :post_id, :body, :presence => true
  attr_accessible :body
end
Первые методы belongs_to указываеют на связь к таблицам posts\users.
С validates понятно у комментария должен быть пользователь\пост\ну и собственно сам текст комментария.
attr_accessible -- (та строчка, из-за которой была паника на GitHub) указывает на то, что можно изменять только это поле (:body), но
не пользователя и пост.
Затем добавляем роутинг.
#/config/routes.rb
resources :posts do
  resources :comments, :only => [:create, :destroy]
end
Тут вложенный ресурс comments ссылки будут выглядеть posts/post_id/comments/
То есть навигация по комментариям идёт уже в определённом посте.
Вот часть сделали. Теперь контроллер
#controllers/comments_controller.rb
class CommentsController < ApplicationController
  load_and_authorize_resource
  def create
    @post = Post.find(params[:post_id])
    comment = @post.comments.build(params[:comment])
    comment.user = current_user

    if comment.save
      flash[:notice] = 'Comment was successfully created.'
      redirect_to post_path(@post)
    else
      flash[:notice] = 'The comment you typed was invalid.'
      render "posts/show"
    end
  end

  def destroy
    post = Post.find([params[:post_id]])
    comment = Comment.find(params[:id])
    comment.delete
    flash[:notice] = "Delete"
    redirect_to post_path(post)
  end

  rescue_from CanCan::AccessDenied do |exception|
    flash[:notice] = "You can not create or delete comment"
    redirect_to post_path(Post.find(params[:post_id]))
  end
end
В контроллере есть методы destroy, который удаляет комментарий и метод create, который создаёт комментарий.
Также видна строчка из cancan load_and_authorize_resource, которую я добавил (а значит изменил и models/ability.rb)
Потом важная вещь переопределил исключение, когда действие не допустимо. Если помните такое же переопределение есть в
ApplicationController так вот там это исключение, если пользователь делает что-то в не своих правах, то будет переброшен
на root_url, а здесь просто возврашаем на тот же пост. Вот и всё.
Методы также просты и не должны вызывать непонимания (чуть что в комментариях задайте вопрос)

Так теперь Ability
#models/ability.rb
class Ability
#...
 def initialize(user)
   #...
   if user.role? :admin
     #...
     can [:create, :destroy], Comment
   else
    if user.role?(:user)
       can :create, Comment
    #...
   end
 end
end
Показал только то, что добавил. Обычный пользователь может комментировать, а админ плюс ко всему и удалять неугодные комментарии)))
Теперь с view.
Форма для создания комментария
#views/comments/_form.html.erb
<% if can? :create, Comment %>
  <%= form_for [@post, current_user.comments.new] do |f| %>
    <%= f.text_area :body, :size => "70%x10" %>
    <br/>
    <%= f.submit "add comment" %>
  <% end %>
<% end %> 
Снова метод can? который проверяет может ли пользователь создавать комментарий. Ну и потом стандартная форма.
Затем для вывода комментариев для поста:
#views/comments/_index.html.erb
<% @post.comments.each do |comment| %>
  <p>
    <b>
      Name:<%= comment.user.email %>
    </b>
    <%= comment.body %>
    <% if can? :clear, Vote %>
      <%= link_to 'delete', post_comment_path(@post, comment), method: :delete %>
    <% end %>
  </p> 
<% end %>
Тут просто выводим email пользователья (потом улучшим будет имя) ну и сам комментарий. А также ссылка для админа, который может удалить комментарий. Формирование ссылки посмотрите, как строится: если мы наберём
rake routes
то покажется куча роутов среди которых есть и post_comment_path, для которого нужен пост и коммент, что мы и передаём ему, а rails сам формирует правильную ссылку.
Теперь добавляем наши view в вывод поста:
#views/posts/show.html.erb
<%= render 'comments/index'%>
<%= render 'comments/form' %>
Просто добавте пару строчек и всё (после вывода поста конечно же, но можете и перед, как угодно).

Ну и теперь куча тестов:
Сначала для PostsController, там же добавили некоторые вещи.
#spec/controllers/post_controller_spec.rb
  describe "GET new" do
    before(:each) {
      @user = Factory(:user)
      @user.roles << Factory(:role) 
      sign_in @user
    }
    it "asigns a new post as @post" do
      get 'new'
      assigns(:post).should be_a_new(Post)
      response.should render_template('posts/new')
    end
    it "asigns a new post as @post if User auth" do
      get 'new'
      assigns(:post).should be_a_new(Post)
      response.should render_template('posts/new')
    end
    it "should redirect_to if user not auth" do
      sign_out @user
      get 'new'
      response.should redirect_to(new_user_session_path)
    end
  end
Надеюсь названия понятны и говорят сами за себя.
Ну и для CommentsController
/spec/controllers/comments_controller_spec.rb
require 'spec_helper'

describe CommentsController do
  describe "POST create" do
    before(:each) {
      @user = Factory(:user)
      @user.roles << Factory(:role)
      @post = Factory(:post, :user => @user)
    }
    it "should create new comment if user can create comments" do
      sign_in @user
      attr = {:body => "new comment"}
      post 'create', {:post_id => @post.id, :comment => attr }
      flash[:notice].should eq('Comment was successfully created.')
      response.should redirect_to(post_path(@post))  
    end
    it "should render posts/show if attr is invalid" do
      sign_in @user
      post 'create', {:post_id => @post.id, :comment => {}}
      flash[:notice].should eq('The comment you typed was invalid.')
      response.should render_template('posts/show')  
    end
    it "should not create new comment if user can not ability" do
      post 'create', {:post_id => @post.id, :comment => {}}
      flash[:notice].should eq("You can not create or delete comment")
      response.should redirect_to(post_path(@post))
    end
  end

  describe "DELETE destroy" do
    before(:each) {
      @post = Factory(:post, :user => Factory(:user))
      comment = Factory(:comment, :user => Factory(:user), :post => @post)
    }
    it "should delete comment if admin" do
      user = Factory(:user)
      user.roles << Factory(:role, :name => :admin)
     
      sign_in user
      
      @post.comments.count.should eq(1)
     
      delete 'destroy', { :post_id => @post.id, :id => @post.comments.last.id }
     
      flash[:notice].should eq("Delete")
      response.should redirect_to(post_path(@post))
      @post.comments.count.should eq(0)
    end
    it "should not delete comment if user" do
      user = Factory(:user)
      user.roles << Factory(:role)
     
      sign_in user
      
      @post.comments.count.should eq(1)
     
      delete 'destroy', { :post_id => @post.id, :id => @post.comments.last.id }
     
      flash[:notice].should eq("You can not create or delete comment")
      response.should redirect_to(post_path(@post))
      @post.comments.count.should eq(1)
    end
  end
end

вторник, 6 марта 2012 г.

Devise + Cancan -- быстрая разработка. Часть вторая.

Привет.
Продолжаем, обещанное вчера -- cancan + devise.
и так создадим ещё одну модель для того, чтобы пользователь мог бы голосовать, ну то есть обычный рейтинг поста.
rails g model vote score:integer
Затем я хочу, чтобы пользователь мог бы только один раз голосовать, поэтому создаю таблицу, где будет показано голосовал
ли пользователь или нет, если что это отношение has_many
#
rails g model users_vote user:references vote:references
#
Остался контроллер для голосования:
rails g controller vote 
Ок, теперь делаем роутинг: открываем config/routes.rb и записываем:
resources :posts do
  resources :vote, :only => [:update] do
    delete 'clear'
  end
end
Что написали? Получается, что ресурс Post содержит в себе ресурс Vote у которого я оставил один стандартный глагол (:update),
а потом говорю, что обращение к методу clear следует через delete метод. Можно было бы записать :only => [:update, :delete], но я хотел именно метод clear, который отражает суть.
Так теперь редактирую модели:
#user.rb
class User < ActiveRecord::Base
 #.........
  has_many :users_votes
  has_many :votes, :through => :users_votes
end
#vote.rb
class Vote < ActiveRecord::Base
  belongs_to :post
  has_many :users_votes
  has_many :users, :through => :users_votes
end
#users_vote.rb
class UsersVote < ActiveRecord::Base
  belongs_to :user
  belongs_to :vote
end
и да обновим Post, ведь нужно же создавать как-то голосование, при создании поста:
class Post < ActiveRecord::Base
  after_create :create_vote #кэлбэк!
  has_one :vote, :dependent => :destroy #не забываем про отношения!

  protected
    def create_vote
      self.vote = Vote.create(:score => 0)
    end
end
Вот всё, как видно связь пользователь и голосов через промежуточную таблицу users_votes Теперь напишем контроллер для голосования:
class VoteController < ApplicationController
  #сразу загружаем ресурсы cancan 
  load_and_authorize_resource
  #а вот и метод update, который увеличивает счётчик голосования 
  def update
    post = Post.find(params[:post_id])
    vote = post.vote
      
    vote.score += 1 if params[:vote][:score] == "Up" 
    vote.score -= 1 if params[:vote][:score] == "Down"
    vote.users << current_user
    vote.save

    flash[:notice] = "You vote counted!"
    redirect_to post_path(post) 
  end
  
  #а этот метод позволяет админу обнулять голосование за пост, 
  #читерство, махинации и подтасовка)
  def clear
    post = Post.find(params[:post_id])
    post.vote.update_attributes(:score => 0)
    
    flash[:notice] = "Clear!" 
    redirect_to post_path(post)
  end
  #также я переопределил исключение, помните в прошлом посте было? 
  #Так вот если оставить стандартное, то 
  #будет перенаправлять на главную, а мне нужно, чтобы оставалось на этой же странице.
  rescue_from CanCan::AccessDenied do |exception|
    flash[:notice] = "You have already voted" 
    redirect_to post_path(Post.find(params[:post_id]))
  end
end
Заметили такую плохую вещь как vote.users << current_user -- я думаю плохой стиль, просто current_user нельзя использовать в моделях, а так бы лучше написать callback. Теперь шаблоны обновим: Простой до невозможности шаблон
#views/vote/_update.html.erb
<% if can? :update, Vote %>
 <%= form_for [@post, @post.vote] do |f| %>
  <%= f.label :score, "+"%>
  <%= f.radio_button :score, "Up" %>
  <%= f.label :score, "-"%>
  <%= f.radio_button :score, "Down" %>
  

  <%= f.submit "vote" %>

 <% end %>
<% end %>

<% if can? :clear, Vote %>
 <%= link_to 'clear', post_vote_clear_path(@post, @post.vote), method: :delete %>
<% end %>
Так этот шаблон рендерится из views/posts/show.html.erb
<%= render 'vote/update' %>
Всё с этим закончили отредактируем Ability (наш cancan)
#models/ability.rb
#Станет такой уже метод
 def initialize(user)
    user ||= User.new

    if user.role? :admin
      can :manage, Post
      can :clear, Vote 
    else
      can :read, :all
      if user.role?(:user)
        can :create, Post
        can :update, Post do |post|
          post.try(:user) == user
        end
      end
    end
    
    unless user.new_record?
    can :update, Vote do |vote|
      !vote.users.include?(user)    
    end
  end
Кто заметил, что теперь админ управляет только Posts? Если бы я оставил :all, то админ мог бы голосовать до бесконечности. Подумайте почему.
Затем добавил привилегию для админа, что он может обнулять голосование.
и добавил проверку на новую запись -- это для гостей, то есть тех, кто не вошёл в систему.
Ну и дальше стандартная штука проверил голосовал ли пользователь .include?(user).
Вот и всё.
Теперь обещанные тесты (только для контроллеров, модели легко проверяются), я не буду давать пояснения к каждому методу, я старался описывать всё,
если что спросите в комментариях. Тесты достаточно понятны, единственное, что может быть не понятно -- это мой английский, которым я владею не очень хорошо ;)
Дальше TL; DR
Сначала фабрики:
#spec/factories.rb
FactoryGirl.define do
  factory :post do
    sequence(:title) {|n| "New Title -- #{n}" }
    content "MyText"
    user
  end
end
#/roles.rb
FactoryGirl.define do
  factory :role do
    name "user"
  end
end
#/users.rb
FactoryGirl.define do
  factory :user do
#здесь генерирую email'ы по возрастанию, потому что пользуюсь 
   #sqlite, для которой Database_cleaner Не действует.
sequence(:email) {|n| "email#{n}@abc.com" } 
    password "1234567"
  end
end
#votes.rb
FactoryGirl.define do
  factory :vote do
    score 0
  end
end
#users_votes.rb - пустая фабрика
FactoryGirl.define do
  factory :users_vote do
  end
end
Сначала для контроллера Post:
require 'spec_helper'

describe PostsController do
  describe "GET index" do
    it "assign all posts" do
      post = Factory(:post)
      get 'index'
      assigns(:posts).should eq([post])
      response.should render_template('posts/index')
    end
  end
  
  describe "GET show" do
    it "should show post" do
      post = Factory(:post)
      get 'show', {:id => post.id}
      assigns(:post).should eq(post)
      response.should render_template('posts/show')
    end 
  end

  describe "GET new" do
    before(:each) {
      @user = Factory(:user)
      @user.roles << Factory(:role) 
      sign_in @user
    }
    it "ssigns a new post as @post if User have ability :user" do
      get 'new'
      assigns(:post).should be_a_new(Post)
      response.should render_template('posts/new')
    end
    it "asigns a new post as @post if User have ability :admin" do
      @user.roles.clear
      @user.roles << Factory(:role, :name => :admin)
      get 'new'
      assigns(:post).should be_a_new(Post)
      response.should render_template('posts/new')
    end
    it "ssigns a new post as @post if User have not ability :user" do
      sign_out @user
      get 'new'
      response.should redirect_to(root_url)
    end
  end

  describe "GET edit" do
    before(:each) {
      @user = Factory(:user)
      @user.roles << Factory(:role)
      @post = Factory(:post, :user => @user)
    }
    it "can edit post if i'm author this post" do
      sign_in @user
      get 'edit', {:id => @post.id }
      assigns(:post).should eq(@post)
      response.should render_template('posts/edit')  
    end
    it "can edit post if i'm the admin" do
      @user.roles.clear
      @user.roles << Factory(:role, :name => :admin)
      sign_in @user
      get 'edit', {:id => @post.id}
      assigns(:post).should eq(@post)
      response.should render_template('posts/edit')   
    end
    it "can not edit post if not author" do
      user = Factory(:user)
      sign_in user
      get 'edit', {:id => @post.id}
      response.should redirect_to(root_url) 
    end
    it "can not edit post if not authorized" do
      get 'edit', {:id => @post.id}
      response.should redirect_to(root_url)   
    end  
  end

  describe "POST create" do
   context "should create post if params is valid" do
      before(:each) {
       @attr = {
        :title   => "new title.",
        :content => "new content."
       }
      }
      it "should create post if user can create post" do
        user = Factory(:user)
        user.roles << Factory(:role)
        sign_in user
        post 'create', {:post => @attr}

        response.should redirect_to(post_path(Post.last))
        assigns(:post).should be_a(Post)
        assigns(:post).should be_persisted 
      end 
      it "redirect_to root_url if user can not create post" do
        post 'create', {:post => @attr}
        response.should redirect_to(root_url)
      end
    end
    context "should not create post if params is not valid" do
      it "should re-render new template if params is invalid" do
        attr = {:title => "new title"}
        
        user = Factory(:user)
        user.roles << Factory(:role)
        sign_in user
        post 'create', {:post => @attr}
        
        response.should render_template('posts/new')
      end 
    end  
  end

  describe "PUT update" do
   before(:each) {
      @user = Factory(:user)
      @user.roles << Factory(:role)
      @post = Factory(:post, :user => @user)
    }
    it "should update post if it is my post" do
      sign_in @user
      put 'update', { :id => @post.id, :post => {:title => 'update title'}}
      response.should redirect_to(post_path(@post))
    end
    it "should update post if i'm the admin" do
      @user.roles.clear
      @user.roles << Factory(:role, :name => :admin)
      sign_in @user
      put 'update', { :id => @post.id, :post => {:title => 'update title'}}
      response.should redirect_to(post_path(@post))  
    end
    it "should not update post if it is not my post" do
      user = Factory(:user)
      user.roles << Factory(:role)
      sign_in user
      put 'update', { :id => @post.id, :post => {:title => 'update title'}}
      response.should redirect_to(root_url) 
    end
    it "should not update post and redirect_to root_url if i can not ability" do
      put 'update', { :id => @post.id, :post => {:title => 'update title'}}
      response.should redirect_to(root_url)
    end   
  end

  describe "DELETE destroy" do
   before(:each) {
     @user = Factory(:user)
      @user.roles << Factory(:role)
      @post = Factory(:post, :user => @user) 
   }
   it "should delete post if i am the admin" do
     @user.roles.clear
     @user.roles << Factory(:role, :name => :admin)
     sign_in @user
     delete 'destroy', { :id => @post.id }
     flash[:notice].should eq("Delete")
     response.should redirect_to(posts_path) 
   end
   it "should not delete post if i am author" do
     sign_in @user
     delete 'destroy', { :id => @post.id }
     response.should redirect_to(root_url) 
   end
   it "should not delete post if i am guest" do
     delete 'destroy', { :id => @post.id }
     response.should redirect_to(root_url) 
   end
  end
end
и вот для Vote
require 'spec_helper'

describe VoteController do
   
   describe "PUT update" do
     before(:each) {
      @user = Factory(:user)
      @user.roles << Factory(:role)
      @post = Factory(:post, :user => @user)
     }
     it "should vote if not voted" do
      sign_in @user
      put 'update', {:post_id => @post.id ,:id => @post.vote.id, :vote => { :score => "Up" } }
        flash[:notice].should eq("You vote counted!")
        response.should redirect_to(post_path(@post))
     end
     it "should not vote if you have already voted" do
      sign_in @user
      put 'update', {:post_id => @post.id ,:id => @post.vote.id, :vote => { :score => "Up" } }
        put 'update', {:post_id => @post.id ,:id => @post.vote.id, :vote => { :score => "Up" } }
        flash[:notice].should eq("You have already voted")
        response.should redirect_to(post_path(@post))
     end
     it "should not vote if you can not abilitiy" do
        put 'update', {:post_id => @post.id ,:id => @post.vote.id, :vote => { :score => "Up" } }
        response.should redirect_to(post_path(@post))
        @post.vote.score.should eq(0)  
     end  
   end

   describe "DELETE clear" do
     before(:each) {
       @user = Factory(:user)
       @user.roles << Factory(:role, :name => :admin)
       @post = Factory(:post, :user => @user) 
     }
     it "should clear if admin" do
       sign_in @user
       delete 'clear', { :id => @post.vote.id, :post_id => @post.id }
      flash[:notice].should eq("Clear!")
       response.should redirect_to(post_path(@post))
     end
     it "should not clear if user" do
       delete 'clear', { :id => @post.vote.id, :post_id => @post.id }
      response.should redirect_to(post_path(@post))
     end
   end
end
Тесты писал интуитивно, поэтому многие куски могут быть лишние. Например меня смущает Создание Ролей.

Вот и всё. Все 24 теста зелёные.

UPD: код на гугле: http://code.google.com/p/example-app/


четверг, 9 февраля 2012 г.

Немного о BDD

   Сейчас совсем короткий пост о BDD.
   Что подрузомевает под собой эти 3 буквы)))
  Всё просто, как всегда -- это техника разработки программ, буквально  разработка программ по их поведению.
   Другая техника -- это TDD, вот как можно описать TDD:
  С чем это можно сравнить, это можно сравнить с тем, как идёшь в темноте в незнакомом
месте. Ударился -- это плохо (тесты не прошли), повторно, здесь уже не ударишься (рефакторинг).
Снова повторно проходим, так до тех пор пока не выйдем (тесты все зелёные).
  BDD же -- это
- установка целей, то есть, что будем делать.
- начинаем писать
- пишем примеры к коду, к каждому методу, буквально к каждой строчке -- это и будет
поведение
- автоматизация примеров.
   Другими словами, если использовать ту же аналогию тёмной комнаты.
- цель -- выйти
- начинаем идти, тут есть за что зацепиться и упасть
- падаем\не_падаем, показывая, где упадём, а где нет.

   Время для примера, после стольких слов.  
  Но сначала, замечание. Сейчас я напишу класс, который в зависимости от количества переданых сторон будет показывать -- это треугольник, квадрат, или пятиугольник. Если количество переданых сторон меньше 3 или больше 5, то будет бросаться исключение. Здесь я не буду рассматривать такие зависимости, что, например в треугольнике сумма двух сторон должна быть больше третье, или что у стороны не может быть нулевой длины, или что у квадрата все стороны равны. 


#example.rb
#включаем в файл необходимую нам библиотеку (RSpec)
require 'rspec'

#сначала класс исключений:

class ShapeExeption < Exception 
end

#сам класс для фигур

class Shape
     attr_reader :iam

     TRIANGLE = "Triangle"
     QUADRATE = "Quardate"
     PENTAGON = "Pentagon"

     def initialize(*args)
         @iam = case args.size
               when 3
                   TRIANGLE
               when 4
                  QUADRATE
               when 5
                  PENTAGON
           end   
         raise ShapeExeption.new "What is this shape?" if @iam.nil?         
      end
end

----------------------------------------------------------------------------
   Сейчас поговрим про этот класс. 
  Вначале я определил, что переменная @iam будем только читаться, но пеопределить её нельзя.
   Затем определил констатны, для треугольника\квадрата\пятиугольника.
  Потом метод инициализации, который принимает какое-то количество аргументов, это показывается *. 
   Потом в блоке case...end определяем, длину полученых аргументов. Ну и для каждой длины своя констатнта, которая укажет, какая фигура.
  В конце метода, есть вызов исключения ShapeException, это когда @iam не получена, в принципе, это же можно было бы и включить в блок case, но я не стал.
  А теперь проверим поведение всего этого кода.

describe "Shape" do

    it "should be Triangle with three arguments" do
          shape = Shape.new(1, 2, 3)
          shape.should be_an_instance_of(Shape)
          shape.iam.should eq(Shape::TRIANGLE)
   end

   it "should be Quardate with four arguments" do
         shape = Shape.new(1, 2, 3, 4)
         shape.should be_an_instance_of(Shape)
         shape.iam.should eq(Shape::QUADRATE)
   end

   it "should be Pentagon with five arguments" do
         shape = Shape.new(1, 2, 3, 4, 5)
         shape.should be_an_instance_of(Shape)
         shape.iam.should eq(Shape::PENTAGON)
   end

    context "Exeption" do
        it "should raise ShapeExeption if arguments not equal [3,4,5]" do
            expect { Shape.new }.to raise_error(ShapeExeption)
            expect { Shape.new(1,2,3,4,5,6) }.to raise_error(ShapeExeption)
        end
     it "should raise NoMethodError when write @iam" do          shape = Shape.new(1, 2, 3)          expect { shape.iam = "abc" }.to raise_error(NoMethodError)      end   end end
   Первой строчкой describe Shape, показывается, что будем проверено поведие класса. 
   Затем идут три метода it(), в каждом проверяется, что наша переменная shape инициализирована  от класса Shape, и проверяем, что возвратит метод iam.
   Потом в контектсе (context) проверяем исключения.
У нас их две, когда значений передано меньше или больше, и когда пытаемся перезаписать @iam.
Это проверяется в блоках expect{}.to raise_error().
   Запускаем:

rspec example.rb --color

.....

Finished in 0.00201 seconds
5 examples, 0 failures

   Вот таким несложным образом было проверяно поведение. 
   Да это программа немного надумана, но отражает действительную суть. Этот класс может быть использован для возврата соответвенно объектов Треугольник\Квадра\Пятиугольник\N-угольник.

четверг, 12 января 2012 г.

Печём для моделей. Тестируем модели в rails

Привет.
Сегодня я покажу, как протестировать модели в rails. Для этого мы будем использовать RSpec - специальная утилита для Ruby. Сделана для BDD.
Сперва устанавливаем необходимые gem'ы.
gem install rspec
gem install factory_girl_rails
gem install rspec-rails
gem install shoulda-matchers
Первых два gem'а это непосредственно rspec и rspec для rails. Третий gem - это фабрика, которуб будет использовать вместо фикстур. Последний для тестирования ассоциаций.


и так у меня есть две модели: Post и Comment.
Таблицы для них элементарные:
- posts
   - title
   - content
 - comments
   - name
   - comment
   - post_id


Пост может иметь много комментариев - зависимость has_many.
Также я проверяю, чтобы все поля были заполнены - валидатор presence, а также для названия поста проверяю длину (от 3 символов).


Таким образом классы:
#/models/post.rb
Class Post < ActiveRecord::Base
   validates :title, :presence => true
   validates :content, :presence => true
   validates :title, :length => { :minimum => 3 }
#асосиации
   has_many :comments
end

#/models/comment.rb
Class Comment < ActiveRecord::Base
   validates :name, :presence => true
   validates :comment, :presence => true
#связываем 
   belongs_to :post
end

Всё, модели готовы.


Теперь напишем элементарные тесты, но для начала нужно включить установленные gem'ы в Gemfile:


#Gemfile
group :test do
  gem "rspec" 
  gem "rspec-rails"
  gem "factory_girl_rails"
  gem "shoulda-matchers"
end

Сейчас генерируем спеки:
rails g rspec:install

Эта команда генерирует директории для контроллеров, моделей, хелперов и так далее.


Отлично. Теперь самое интересное пишем тесты спеки.


#/spec/models/post_spec.rb
require 'spec_helper'

describe Post do
  #сначала проверяем валидаторы
  context "validates" do
     before(:each) do
        @post = Factory.create(:post)
     end
     
     #должна быть валидная, если валидные атрибуты (title, content)
     it "is valid with valid attributes" do
       @post.should be_valid
     end
     
     #не валидная без title   
     it "is not valid without a title" do
        @post.title = nil
        @post.errors_on(:title).should include("can't be blank")  
        @post.should be_valid
     end
     
     #не валидная без content'а
     it "is not valid without a content" do
               @post.content = nil
               @post.errors_on(:content).should include("can't be blank")
               @post.should_not be_valid
          end
     
      #не валидная, если длина названия, меньше чем 3 символа
           it "is not valid if length of title less then 3 characters" do
               @post.title = "12"
               @post.should_not be_valid
           end
      
      #валидная с длиной больше, чем 3 символа
           it "is valid if length of title greater then 3 characters" do
               @post.title = "123"
               @post.should be_valid
           end
   end
end
Что я использовал здесь.
Сперва я подписался на Post, (describe Post)
Затем я разбил всё на контексты (context), первый это валидаторы.
Затем методами it, я проверял утверждения, например: Длина названия должна быть больше 3 символов - это просто утверждение, проверяется в методе it. Но перед вызовами каждого теста, я устанавливал @post - это происходит в методе before(:each). Проверка происходит методами RSpec, should, should_not (должен и не должен). Например:


      #валидная с длиной больше, чем 3 символа
           it "is valid if length of title greater then 3 characters" do
               @post.title = "123"
               @post.should be_valid
           end


будет звучать так: Если длина названия содержит более чем 3 символа, то пост валиден.


Тоже самое пишем и для модели Comment


#/spec/models/comment_spec.rb
describe Comment do
   context "validates" do 
       before(:each) do
             @comment = Factory.create(:comment)
        end

   
        it "is valid with valid attributes" do
              @comment.should be_valid
          end

          it "is invalid without name" do
             @comment.name = nil
             @comment.should_not be_valid
          end

          it "is invalid without comment" do
             @comment.comment = nil
             @comment.should_not be_valid
          end
     end
 end
end


Как видем тоже самое, проверка имени и комментария.
Что дальше, теперь напишем тесты для ассоциаций, всё это можно было бы сделать и без shoulda, но с ним более удобно. Но не удобно (для меня) тестировать валидаторы, поэтому я применяю этот gem для тестирования ассоциаций.
Добавляем контексты


#/spec/models/post_spec.rb

context "associations" do
    it "should have many comments depending" do
               should have_many(:comments) #пост имеет много комментариев
         end
end
#/spec/models/comment_spec.rb
context "associations" do
       it "should belongs_to post" do
          should belong_to(:post) #комментарии должны быть связаны с постом
       end
end
Теперь, что с Factory Girl (хорошее название)
фактори делает не нужными фикстуры, кто с ними работал, тот знает, что написание фикстур это утомительно, очень утомительно.


#/spec/factories/factoryes.rb

FactoryGirl.define do
   factory :post do
       title "Title"
       content "Content"
   end
end

FactoryGirl.define do
   factory :comment do
        name "Foo"
        comment "Bar"
        post
   end
end
Когда мы делаем Factory.create(:post), то получаем модель с  задаными атрибутами. Всё просто, не фикстуры.


Теперь запускаем тесты, так как я написал спеки только для моделей, то и запускать буду для моделей:
rake spec:models
===>
/home/xxx/.rvm/rubies/ruby-1.9.3-p0/bin/ruby -S rspec ./spec/models/post_spec.rb ./spec/models/comment_spec.rb
..........


Finished in 0.13836 seconds
10 examples, 0 failures


 
Ссыли:


https://github.com/thoughtbot/shoulda-matchers
https://github.com/rspec/rspec-rails
https://github.com/thoughtbot/factory_girl