Popular Posts

вторник, 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/


понедельник, 5 марта 2012 г.

Devise + Cancan -- быстрая разработка


Привет.
Сегодня посмотрим как работать с devise и cancan. Важные gem'ы, да?
и так сперва задача:

  •   Авторизация\регистрация\аутентификация пользователя на сайте -- это задача devise
  •   Потом привилегии: 
  •   админ может всё ;)
  • пользователь может создавать посты
  • автор постов (владелец) может его редактировать, но удалять не может.

Не очень сложно.
и так в Gemfile добавляем
gem 'devise'
gem 'cancan'

Ok, установили теперь devise:
rails g devise:install
Эта команда создаст файл инициализации, нам нужно его отредактировать:
 config/initializers/devise.rb
находим строчку
 config.sign_out_via = :delete
меняем на :get.
Так хорошо. Теперь делаем модель пользователя:
rails g devise user
Создаёт модель. Ну и сразу делаем миграцию, что создаёт таблицу в БД:
rake db:migrate
импортируем views
rails g devise:views
Ок. Теперь делаем для постов, тут легче:
rails g scaffold Post title:string content:text user:references
rake db:migrate
Всё! Что хотели, то сделали: можно регистрироваться и все дела.
Теперь время добавить привилегии с cancan:
rails g cancan:ability
Создаёт класс возможностей\привилегий в app/models.
Ну в принципе каркас написан, осталось добавить немного кода.
Но перед этим нужны же ещё Роли (Role). У пользователя может быть тысяча ролей, и модератор и просто_пользователь в общем придумайте сами.
У меня -- это будет admin\user и всё. и так как видно это отношение has_many:through


rails g model role name:string
rails g model users_role user:references role:references


Всё теперь редактируем модель User, добавляем зависимости:
class User < ActiveRecord::Base
  has_many :users_roles
  has_many :roles, :through => :users_roles
end

Модель Role
class Role < ActiveRecord::Base
  has_many :users_roles
  has_many :users, :through => :users_roles
end


Всего лишь строчку в PostController:
load_and_authorize_resource
и всё.
Теперь с представлением:
Ну допустим раньше выводилось вот так:
#views/index.html.erb
<% @posts.each do |post| %>
  <tr>
    <td><%= post.title %></td>
    <td><%= post.content %></td>
    <td><%= post.user %></td>
    <td><%= link_to 'Show', post %></td>
    <td><%= link_to 'Edit', edit_post_path(post) %></td>
    <td><%= link_to 'Destroy', post, confirm: 'Are you sure?', method: :delete %></td>
  </tr>
<% end %>
<br />
<%= link_to 'New Post', new_post_path %>
Теперь с помощью волшебного метода can делаем:
<h1>Listing posts</h1>


<table>
  <tr>
    <th>Title</th>
    <th>Content</th>
    <th>User</th>
    <th></th>
    <th></th>
    <th></th>
  </tr>


<% @posts.each do |post| %>
  <tr>
    <td><%= post.title %></td>
    <td><%= post.content %></td>
    <td><%= post.user %></td>
    <% if can? :read, post %>
      <td><%= link_to 'Show', post %></td>
    <% end %>
    <% if can? :update, post %>
      <td><%= link_to 'Edit', edit_post_path(post) %></td>
    <% end %>
    <% if can? :destroy, post %>
      <td><%= link_to 'Destroy', post, confirm: 'Are you sure?', method: :delete %></td>
    <% end %>
  </tr>
<% end %>
</table>


<% if can? :create, Post %>
  <%= link_to 'New Post', new_post_path %>
<% end %>
Также не забывайте в контроллере PostsController в методе create устанавливать связь пользователя  и нового поста:
def create
    @post = current_user.posts.new(params[:post])
  ....
end

Потом напишем немного больше и продолжим с cancan + добавим спеки (rspec)
Немного UPD
Чтобы сделать меню, где вход\выход -- стандратное меню, нужно например в views/layouts/application.html.erb
добавить что-то вроде этого:

<div id="header">
  <%= render "shared/links" %>
</div>
Папку shared создайте сами, и в ней файл _links.html.erb

<ul>
  <% if user_signed_in? %>
    <li><%= link_to "My Profile", edit_user_registration_path%> </li>
    <li><%= link_to "Sign out", destroy_user_session_path%> </li>
  <% else %>
    <li><%= link_to "Sign up", new_user_registration_path%> </li>
    <li><%= link_to "Sign in", new_user_session_path%> </li>
  <% end %>
</ul>   

Потом для правильно отрисовки в app/helpers/application_helper.rb:

module ApplicationHelper
  def resource_name
    :user
  end


  def resource
    @resource ||= User.new
  end


  def devise_mapping
    @devise_mapping ||= Devise.mappings[:user]
  end
end

Ok, всё круто, но я чувствую у вас вопрос: Откуда взять роль, когда пользователь зарегистрировался? Ответ -- callbacks
В модели User:
#models/user.rb
class User < ActiveRecord::Base
   before_create :create_role
  
  has_many :posts #надеюсь вы не забыли установить эту зависимость :)
  private
    def create_role
      self.roles << Role.find_by_name(:user)  
    end
end
Ещё UPD
Когда мы ищём роль: Role.find_by_name(:user), то предпологается, что уже есть такая роль. Поэтому перед запуском приложения в файле db/seeds.rb
Role.create(:name => :admin)
Role.create(:name => :user)
И потом в консоли rake db:seed


UPD код: http://code.google.com/p/example-app/


Прошлый пост

вторник, 28 февраля 2012 г.

PrototypeJS Классы, Наследование и Модули.

Привет!
Сегодня важная тема, я бы сказал -- это наследование и классы и объекты в prototype, ну также всё, что будет показано относится в равной мере и ко всему javascript.

Проблема

Как всегда сначала описание проблемы. Предположим у меня есть мебель: шкаф и диван. Ну да немного, но для задачи достаточно. Как это выглядит со стороны объектов? Понятно, что и то, и то -- мебель, ведь так? Но на диване я сижу, лежу, и так далее, а в шкафу куча дверек, в шкаф я складываю одежду\посуду\да_что_угодно. Но также есть такие общие вещи как цвет. Вот.
Теперь непосредственно проблема, по идее диван и шкаф виды мебели, но диван и шкаф могут иметь разные свойста (для сиденье? сколько двёрок? и так далее). Как же это описать?
Вот тут-то и появляются так называемые MixIn, или модули, которые могут быть в новинку для javascript программистов, но отнюдь не новы для тех, кто пишет на ruby. Но благодаря, природе javascript в нём можно спокойно делать модули и разбавлять объекты. Я написал много слов, теперь напишу код.
И так. Сначала практика создания классов в prototypejs
Тут просто:
var SomeClass = Class.create({
    initialize: function(){}
});
Как просто, да? Сначала делаем класс Class.create, в нём нужна функция конструктор (но она не обязательна) initialize, что сродни ruby методу с таким же названием. Это функция будет инициализироваться при создании объекта. Всё просто.
Всё теперь пишу нужный код:
var Furniture = Class.create({
    initialize: function(title, price)
    {
      this.title = title;
      this.price = price;
    },
    toString: function()
    {
          return this.title + ": " + this.price + "$"
    }
    });
Две функции, инициализации, и вторая функция для вывода информации о мебели.
Этот класс в java я бы сделал абстрактным, потому что просто мебели как бы не существует, это абстаркция, мебель это всегда что-то -- стул, стол, шкаф...

Теперь пишем классы для Шкафа и Дивана:
var Sofa = Class.create(Furniture, {
    initialize: function($super, title, price)
    {
      $super(title, price);
    }
});
var Cupboard = Class.create(Furniture, {
    
    initialize: function($super, title, price)
    {
      $super(title, price);
    }
});
Как вы, надеюсь, поняли, здесь мы наследуем от мебели (Furniture) с помощью Class.create(ExtObj, {}), где ExtObj -- это "родитель". В инициализаторе вызываем $super, это
заставляет проинициализировать код функции родителя. Про $super, читайте в конце поста.

Ok, я это сделал, теперь время подмешать модули.
Пусть есть такие свойста: цвет, можно ли сидеть, сколько дверек в мебели.
//для цвета
var Color = {
      set_color: function(color)
      {
        this.color = color;
      },
      get_color: function()
      {
        return this.color;
      }
};
//Я могу сесть на эту мебель?
var ForSeating = {
    for_seating: function()
    {
      return true;
    }
};
//Сколько дверек в этой мебели?
var ManyDoors = {
    set_doors: function(doors)
    {
        his.doors = doors;
    },
       how_many: function()
    {
      return this.doors;
    }
};
Ok, круто, как видим все эти модули просты Object\ы  в javascript.
Теперь самое интересное и простое, как подмешать? Для этого есть методы Object, который расширяет Prototype -- addMethods()
Получаем:
//Для дивана модуль который говорит, что можно сидеть и модуль цвета
Sofa.addMethods(ForSeating);
Sofa.addMethods(Color);
//Для шкафа цвет и дверки.


Cupboard.addMethods(Color);
Cupboard.addMethods(ManyDoors);
Что получим?

Вот результаты:
Сначала создаём новые:
var sofa = new Sofa('Cool Sofa', 10);
var cupboard = new Cupboard('My Cupboard', 12);


//Устанавливаем цвета и двери для шкафа
sofa.set_color('red');

cupboard.set_color('black');
cupboard.set_doors(3);

//А вот результат уже
sofa.get_color(); // => 'red'
sofa.for_seating(); // => true
cupboard.get_color(); // => 'black'
cupboard.how_many(); // =>  3
Если мы вызовем для шкафа метод, который скажет можно ли на нём сидеть? то получим



Вот так.

Совсем маленький рефакторинг.

Как мы видили, Модуль Color добавляется и в Sofa и в Cupboard, поэтому лучше "подмешать" в родительский класс:

Furniture.addMethods(Color);

Теперь методы этого модуля будут доступны в обоих объектах.

Для чего всё это нужно?

Это позволяет вынести общие методы для разных объектов, что упростит жизнь, ведь кода нужно меньше, вы не будете повторять себя.
Сделает понятнее программу.
Какие возможные траблы?
Возможно затереть методы, очень часто есть методы set()\get(), которые могут быть реализованы для разных модулей, в итоге останется только один, как горец))). Также сложность следить за программой, у вас в модуле появится ссылка this, на объкт + куча свойств объекта, не сразу понятно как этим пользоваться, но это просто, так же в связи с этим можно случайно переопределить свойство, поэтому рекомендую не полениться и написать get_property()\set_property(), так лучше.

Я написал много слов об недостатках, но это для того, чтобы предупредить о сложности, на самом деле плюсов больше.

Примечание: Наследование PrototypeJS

Снова немного кода, для пояснения некоторых вещей.  Есть несколько классов:
var A = Class.create({
    say: function()
    {
      alert('A');
    },
    method: function()
    {
      alert('method A');
    }
    });
    var B = Class.create(A, {
    say: function()
    {
      alert('B');
    }
    });
    var C = Class.create(A, {
    say: function($super)
   {
      $super();
      alert('C');
    },
    abc: function()
    {
      alert('abc');
    }
    });
   
    var a = new A();
    var b = new B();
    var c = new C();
    a.say(); // => A
    b.say(); // => B
    c.say(); // => A, C
    c.method(); // => method A
Здесь видим наследование B и C наследует A. Но в B мы пишем свой метод say(). а в C вызываем сначала родительский $super() -- это пригодится, когда нужно установить некоторые свойста, которые определяются в родительском, например при инициализации. Также видим, что метод method() из A доступен и в C дочернем классе.  Доки






суббота, 11 февраля 2012 г.

BDD в javascript. Jasmine

Сегодня я начну серию постов о тестировании Javascript кода. Многие из разработчиков уделяют этому мало времени, считая, что это не стоит внимания, потому что javascript -- это детский язык, который можно выучить за пару дней. Это конечно же ошибка, чтобы понять суть javascript  у некоторых уходит достатточно много времени,  и они крайне удивляются особенностям javascript, например:
 "1" + 3 == 13 // true
 "10" - 3 == 7 // true
Я напишу несколько постов сначала jasmine, затем фреймворк для Prototypejs & scriptaculous. В серии jasmine будет сначала просто работа с jasmine, потом работа с ajax & DOM, потом интерграция c prototypejs и jquery. Jasmine из Ruby, Потом уже Unit тесты от Томаса Фукса (script.aculo.us)
Но это совсем не значит, что я не буду писать о другие посты.
и так поехали.

Jasmine -- это BDD фреймворк для тестирования javascript кода. Довольно простой, не зависит от других фреймворков, то есть был написан для javascript кода, а не для prototype\jquery\yui.

Синтаксис.


Jasmine унаследовало или скорее поддерживает синтаксис основного bdd фреймворка -- RSpec. Тесты состоят из двух частей describe() -- указывает поведение, какой-то общий контекст для которого выполняются тесты. Вторая часть -- it(), простое утверждение. Describe() и it() принимают два аргумента, первый -- это строка, которая показывае, что вы делаете. Второй -- это функция, непосредственно тест.

Проект

Допустим мы скачали jasmine с сайта. Теперь посмотрим какова структура проекта (директорий).
- jasmine-standalone
   -- lib
      --- jasmine-1.1.0
           ---- некоторые файлы
  -- src
     -- //ваши файлы тестируемые
 -- spec
     -- // ваши тесты
spec_runner.html //запуск тестов, в этот файл скопируйте просто отсюда и пропишите свои пути к файлам тестов и своего кода.

Вот достаточно просто. Для запуска тестов просто открываем spec_runner.html в  любом браузере. Смотрим какие прошли, какие не прошли и исправляем.

Написание тестов

Но прежде, чем тестировать нужно что-то написать для тестов. У меня есть вполне реальный пример кода, который я использую, это определитель високосного года.
function isLeapYear(year) {

       if (year % 4 == 0) {
                if (year % 100 == 0) {
                        if (year % 40 == 0) {
                                return true;
                       }
                     return false;
                }
                return true;
       }
      return false;
}; 

Работа понятна принимает значение года, возратит true или false, в зависимости от принятого года.

Вот теперь можно и писать тесты:
describe("isLeapYear", function() {
    it("2004 should be leap year", function() {
        expect(isLeapYear(2004)).toBeTruthy();
        expect(isLeapYear(2004)).toEqual(true);
    });
   it("2000 should be leap year", function() {
      expect(isLeapYear(2000)).toBeTruthy();
   });
   it("1700 should not be leap year", function() {
      expect(isLeapYear(1700)).toBeFalsy();
   });
   it("2001 should not be leap year", function() {
     expect(isLeapYear(2001)).toBeFalsy();
   });
});
Как видим в describe я указал, что будут тесировать функцию isLeapYear, хотя тут можно было указать что угодно. А потом идут утверждения it(). В блоке которых я проверяю с expect(), за expect() идёт некотрое утверждение (matchers), которых несколько в jasmine. Всё довольно просто. Но наши тесты не могут быть полными, ведь я могу отправить вместо числа строку или массив или ничего, поэтому улучшаем функцию.
function isLeapYear(year) {
   if (typeof(year) != 'number')
           throw 'Year should be Number';
 ...
}
Если тип аргумента не номер, будет брошено исключение.
Дописываем необходимые тесты:
it("Not_Number should throws exception", function() {
    expect(function() { isLeapYear('Not_Number'); }).toThrow('Year should be Number');
    expect(function() { isLeapYear(); }).toThrow('Year should be Number');
});
Всё отлично работает:

beforeEach & afterEach

Сейчас рассморим работу только beforeEach, так как afterEach будет  работать также. Сейчас я напишу некоторую функция-класс, которая будет делать, то же что и isLeapYear:
var Year = function() {
    this.isLeapYear = function(year) {
            return isLeapYear(year);
    };   
};
Видим класс возвращает просто функцию isLeapYear.
Напишем тесты, но уже  с beforeEach:
describe("Object Year.isLeapYear", function() {

   beforeEach(function () {
          Obj = new Year();
     });

     it("2004 should be leap year", function() {
     expect(Obj.isLeapYear(2004)).toBeTruthy();
   });
});
В beforeEach устанавливаем Obj, который будет экземпляром Year. А в блоке it(), проверяем метод этого объекта Obj.isLeapYear(). Я не писал все тесты, потому что они будут подобны прошлым. Вот результат:

Пишем свой mathcer

Написание свойго утверждения в jasmine довольно просто. Может возникнуть зачем новый mather? Всё довольно просто -- это устраняет дублирование кода, а также улучшает кода, ведь не нужно писать большие манипуляции, а достаточно написать один раз новый matcher.
Matcher -- это функция, которая будет принимать сколько угодно аргументов (всё зависит от того, что вы проверяете) и манипулирует переменной this.actual -- это поступает из expect() блока. Matcher добавляется с помощью this.addMatchers({}) в блоке beforeEach(), возвратить ваш matcher должен либо true, либо false, и так когда есть немного теории.
beforeEach(function() {
    this.addMatchers({
         toBeLeapYear: function () {
            var year = this.actual;

                       if (typeof(year) != 'number')
                          throw 'Year should be Number';

                      if (year % 4 == 0) {
                      if (year % 100 == 0) {
                         if (year % 40 == 0) {
                             return true;
                         }
                         return false;
                      }
                      return true;
                   }
                   return false;
                  } 
    });
});
Как видим -- это наша старая функция isLeapYear().
Теперь спеки:
it("2004 should be leap year", function() {
    expect(2004).toBeLeapYear();
    expect(2000).toBeLeapYear();
});
it("2001 fail test", function() {
    expect(2001).toBeLeapYear();
});

Здесь один тест не пройдёт, что и ожидалось.

---------------------
Что не было рассмотрено: шпионы и асинхронные спеки









четверг, 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-угольник.

среда, 8 февраля 2012 г.

Acts-as-taggable-on -- делаем тэги.

Наверное одной из самых популярных вещей, которые реализуют буквально все, это тэггирование постов, фотографий, всего. Тэги помогают лучше ориентироваться, ведь если что-то находится в категории например "javascript", то это совсем не значит, что во всех постах будет описываться работа с ajax или с prototypejs. Поэтому тэги помогают отобрать из пачки постов\фотографий нужные.
Как же это делается в rails?
Всё просто, есть gem acts-as-taggable-on, который позволяет расставлять тэги. Я покажу как это делается для постов.
Что у нас есть? Во-первых -- это модель Post + контроллер + представления.
Всё это сгенерировано
$ rails g scaffold post title:streing,  content:text
Затем устанавливаем сам gem в Gemfile, просто добавляя строчку:
gem 'acts-as-taggable-on'
и выполняем bundle install. Всё установлено.
Затем добавляем в модель следующее:
class Post < ActiveRecord::Base
  acts_as_taggable_on :tags   # <= это добавили
  attr_accessible :title, :content,  :tag_list # <= и здесь записываем tag_list, если не запишем, то не сможем посмотреть тэги, будет вызвана ошибка.
end
В контроллер также добавляем метод.
class PostsController < ApplicationController
    #для вывода облака тэгов
    def tag_cloud
        @tags = Post.tag_counts_on(:tags)
    end
  
   def index
       @posts = Post.all
       tag_cloud  # <= отсюда обращаемся, чтобы получить все тэги
   end
end
Всё, теперь в представлении, я добавил, в application.html.erb, это для того, чтобы видно было на всех страницах, если вам нужно, чтобы облако было только на какой-то определённой странице, добавляйте в представление той страницы.
<% tag_cloud(@tags, %w(css1 css2 css3 css4)) do |tag, css_class| %>
          <%= link_to tag.name, { :action => :tag, :id => tag.name }, :class => css_class %>
<% end %>


css1 css2 css3 css4 -- классы стилей для тэгов, от большого до маленького.

Вот собственно и всё. Проще, чем кажется на первый взгляд.
Документация по gem'у покажет некоторые хорошие вещи, которые можно сделать.

преведущий пост


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

Ruby Koans

   Многие сталкиваются с проблемой, что прочитам несколько книг, и не почти не написав  ни строчки кода(!), хотят проверить свои знания.
   Для этого есть один хороший проект нацеленый на это это Ruby Koans.
   Проект построен на базе тестов, нет не тех тестов где нужно выбрать один вариант из нескольких, а TDD (то самое про которое писал Кент Бек).
   В koans нужно выполнить 280 тестов, каждый тест содержит несколько "утверждений" (assertion). Например простейший тест:
def test_creating_arrays
      empty_array = Array.new
      assert_equal __, empty_array.class
      assert_equal __, empty_array.size
end
Вместо __ нужно вписать правильные ответы:
def test_creating_arrays
      empty_array = Array.new
      assert_equal Array, empty_array.class
      assert_equal 0, empty_array.size
end
Таким образом нужно освоить 280 тестов, но там не только нужно вписывать свои ответы, также есть возможность написать свои классы, то есть есть на чём потренироваться в написании кода.
В конце увидите, если все 280 тестов правильны:


Last post