Rails::Test[RSpec+Guard+Spork+Cucumber+Capybara+factory_girl+Faker]

Create a Rails project

$ rails new sample_app --skip-test-unit

Gemfile

source 'https://rubygems.org'

gem 'rails', '3.2.8'
gem 'bootstrap-sass', '2.0.4'
gem 'bcrypt-ruby', '3.0.1'
gem 'faker', '1.0.1'
gem 'will_paginate', '3.0.3'
gem 'bootstrap-will_paginate', '0.0.6'
gem 'jquery-rails', '2.0.2'

group :development, :test do
  gem 'sqlite3', '1.3.5'
  gem 'rspec-rails', '2.11.0'
  gem 'guard-rspec', '1.2.1'
  gem 'guard-spork', '1.2.0'  
  gem 'spork', '0.9.2'
end

# Gems used only for assets and not required
# in production environments by default.
group :assets do
  gem 'sass-rails',   '3.2.5'
  gem 'coffee-rails', '3.2.2'
  gem 'uglifier', '1.2.3'
end

group :test do
  gem 'capybara', '1.1.2'
  gem 'factory_girl_rails', '4.1.0'
  gem 'cucumber-rails', '1.2.1', :require => false
  gem 'database_cleaner', '0.7.0'
  # gem 'launchy', '2.1.0'
  # gem 'rb-fsevent', '0.9.1', :require => false
  # gem 'growl', '1.0.3'
end

group :production do
  gem 'pg', '0.12.2'
end

RSpec

$ rails generate rspec:install

生成不带test framework的控制器

$ rails generate controller StaticPages home help --no-test-framework

Generate integration test

$ rails generate integration_test static_pages

As this

require 'spec_helper'

describe "Static pages" do

  describe "Home page" do

    it "should have the content 'Sample App'" do
      visit '/static_pages/home'
      page.should have_content('Sample App')
    end
  end
end

执行测试

$ bundle exec rspec spec/requests/static_pages_spec.rb

Guard

Initialize guard with RSpec

$ bundle exec guard init rspec

Start guard

$ bundle exec guard

Spork

Bootstrap the Spork configuration

$ bundle exec spork --bootstrap

See the time

$ time bundle exec rspec spec/requests/static_pages_spec.rb

Start spork

$ bundle exec spork

See the time again

$ time bundle exec rspec spec/requests/static_pages_spec.rb --drb

Configuring RSpec to automatically use Spork

.rspec

--colour
--drb

Control-C && restart

$ bundle exec spork

Guard with Spork

$ bundle exec guard init spork

Start Guard and Spork at the same time

$ bundle exec guard

Cucumber

Install cucumber

$ rails generate cucumber:install

This creates a features/ directory

Create a file

features/signing_in.feature

Feature: Signing in

  Scenario: Unsuccessful signin
    Given a user visits the signin page
    When he submits invalid signin information
    Then he should see an error message

  Scenario: Successful signin
    Given a user visits the signin page
      And the user has an account
      And the user submits valid signin information
    Then he should see his profile page
      And he should see a signout link

Run the features

$ bundle exec cucumber features/

RSpec, Cucumber can be invoked using a Rake task

$ bundle exec rake cucumber

Capybara

Steps needed to get the signin features to pass

Given /^a user visits the signin page$/ do
  visit signin_path
end

When /^he submits invalid signin information$/ do
  click_button "Sign in"
end

Then /^he should see an error message$/ do
  page.should have_selector('div.alert.alert-error')
end

Given /^the user has an account$/ do
  @user = User.create(name: "Example User", email: "user@example.com",
                      password: "foobar", password_confirmation: "foobar")
end

When /^the user submits valid signin information$/ do
  fill_in "Email",    with: @user.email
  fill_in "Password", with: @user.password 
  click_button "Sign in"
end

Then /^he should see his profile page$/ do
  page.should have_selector('title', text: @user.name)
end

Then /^he should see a signout link$/ do
  page.should have_link('Sign out', href: signout_path)
end

Run again,the Cucumber tests should pass

$ bundle exec cucumber features/

factory_girl

Defining a Factory Girl sequence

spec/factories.rb

FactoryGirl.define do
  factory :user do
    sequence(:name)  { |n| "Person #{n}" }
    sequence(:email) { |n| "person_#{n}@example.com"}   
    password "foobar"
    password_confirmation "foobar"
  end
end

Faker

A Rake task for populating the database with sample users

lib/tasks/sample_data.rake

namespace :db do
  desc "Fill database with sample data"
  task populate: :environment do
    User.create!(name: "Example User",
                 email: "example@railstutorial.org",
                 password: "foobar",
                 password_confirmation: "foobar")
    99.times do |n|
      name  = Faker::Name.name
      email = "example-#{n+1}@railstutorial.org"
      password  = "password"
      User.create!(name: name,
                   email: email,
                   password: password,
                   password_confirmation: password)
    end
  end
end

Invoke the Rake task

$ bundle exec rake db:reset
$ bundle exec rake db:populate
$ bundle exec rake db:test:prepare

Useful links

status_tag’s custom helper

Helper

../app/helpers/application_helper.rb

module ApplicationHelper

  def status_tag(boolean, options={})
    options[:true]        ||= ''
    options[:true_class]  ||= 'status true'
    options[:false]       ||= ''
    options[:false_class] ||= 'status false'

    if boolean
      content_tag(:span, options[:true], :class => options[:true_class])
    else
      content_tag(:span, options[:false], :class => options[:false_class])
    end
  end

  # ...

end

View

../app/views/pages/list.html.erb

...
<td class="center"><%= status_tag(page.visible) %></td>
...

Stylesheet

../app/assets/stylesheets/application.css

span.status { display: block; width: 10px; height: 10px; margin: 0.25em auto; padding: 0; border: 1px solid #000000; }
span.status.true  { background: green; color: green; }
span.status.false { background: red; color: red; }

Tutorial

Ruby on Rails 3 Essential Training
10. Layouts, Partials, and View Helpers

  • 1009 Custom helpers

又一种Form errors写法

视图

../app/views/shared/_error_messages.html.erb

<% if object && object.errors.size > 0 %>
  

prohibited this record from being saved

There were problems with the following fields:

<% end %>

帮手

../app/helpers/application_helper.rb

module ApplicationHelper

  # ...

  def error_messages_for( object )
    render(:partial => 'shared/error_messages', :locals => {:object => object})
  end

end

视图

../app/views/pages/_form.html.erb

<%= error_messages_for(@page) %>
...

参考教程

Ruby on Rails 3 Essential Training 11. Forms

  • Form errors

移动数据的位置(position_mover)

创建一个lib

lib/position_mover.rb

# PositionMover is a module to manage the :position
# attribute of a model.
# Put in the "/lib" folder and include it in any class 
# (with a :position column) using "include PositionMover".
# NB: Plug-ins such as "acts_as_list" offer additional 
# functionality and better performance.
module PositionMover

  # <tt>move_to_position</tt> is an instance method that 
  # will move a list item to a new position, but also 
  # increment/decrement the positions of the other list items
  # as necessary.
  # 
  # Send nil as the value for new_position to remove 
  # the item from the list.
  def move_to_position(new_position)
    max_position = self.class.where(position_scope).count
    # ensure new_position is an integer in 1..max_position
    unless new_position.nil?
      new_position = [[1, new_position.to_i].max, max_position].min
    end

    if position == new_position # do nothing
      return true
    elsif position.nil? # not in list yet
      increment_items(new_position, 1000000)
    elsif new_position.nil? # remove from list
      decrement_items(position+1, 1000000)
    elsif new_position < position # shift lower items up
      increment_items(new_position, position-1)
    elsif new_position > position # shift higher items down
      decrement_items(position+1, new_position)
    end
    return update_attribute(:position, new_position)
  end

  private

  # <tt>position_scope</tt> defines an SQL fragment used for 
  # narrowing the scope of queries related to position.
  #
  # Often it is not desirable to manage positions for all items 
  # in a class, but instead to narrow the scope based on a 
  # parent class. For example, if subject1 has 3 pages and
  # subject2 has 4 pages, you would want to narrow the scope of 
  # position so that when working with subject1's pages only 
  # positions 1-3 under that subject are reordered. The positions
  # of subject2's pages should be unchanged. And each subject 
  # should be able to have a page at position 1.
  #
  # To narrow the scope, override this method in your model 
  # with the SQL that should be used to narrow the scope. 
  # (NB: Must come after "include PositionMover".)
  # 
  # Example:
  # class Page < ActiveRecord::Base
  #   include PositionMover
  #   def position_scope
  #     "pages.subject_id = #{subject_id.to_i}"
  #   end
  # end
  def position_scope
    # default is always true
    # won't affect SQL conditions or narrow scope
    "1=1"
  end

  def increment_items(first, last)
    items = self.class.where(["position >= ? AND position <= ? AND #{position_scope}", first, last])
    items.each {|i| i.update_attribute(:position, i.position + 1) }
  end

  def decrement_items(first, last)
    items = self.class.where(["position >= ? AND position <= ? AND #{position_scope}", first, last])
    items.each {|i| i.update_attribute(:position, i.position - 1) }
  end

end

模型

models/page.rb

require 'lib/position_mover'
class Page < ActiveRecord::Base

    include PositionMover

    # ...

    scope :sorted, order('pages.position ASC')

    private

    def position_scope
      "pages.subject_id = #{subject_id.to_i}"
    end

end

控制器

controllers/pages_controller.rb

class PagesController < ApplicationController

    before_filter :find_subject

    def new
      @page = Page.new(:subject_id => @subject.id)
      @page_count = @subject.pages.size + 1
      # ...
    end

    def create
      new_position = params[:page].delete[:position]
      @page = Page.new(params[:page])
      if @page.save
        @page.move_to_position(new_position)
        # ...
      else
        # ...
      end
    end

    def edit
      @page = Page.find(params[:id])
      @page_count = @subject.pages.size
      # ...
    end

    def update
      @page = Page.find(params[:id])
      new_position = params[:page].delete[:position]
      if @page.update_attributes(params[:page])
        @page.move_to_position(new_position)
        # ...
      else
        # ...
      end
    end

    def destroy
      page = Page.find(params[:id])
      page.move_to_position(nil)
      page.destroy
      # ...
    end

    private

    def find_subject
      if params[:subject_id]
        @subject = Subject.find_by_id(params[:subject_id])
      end
    end
end

视图

views/pages/_form.html.erb

<tr>
  <th><%= f.label(:position) %></th>
  <td><%= f.select(:position, 1..@page_count) %></td>
</tr>

参考教程

Ruby on Rails 3 Essential Training 14. Improving the Simple CMS

  • 1403 Scoping the sort positions
  • 1404 Managing sort positions
  • 1405 Using the positionMove module

Linux&&Heorku添加环境变量的方法

本机(如Ubuntu)

如果用这种方法,是临时的

$ export MY_DATA=data_value

所以要这样

$ vim ~/.bashrc
# 在最后添加
export MY_DATA=data_value

再执行

source ~/.bashrc

Heroku

$ heroku config:add GITHUB_USERNAME=joesmith
Adding config vars and restarting myapp... done, v12
GITHUB_USERNAME: joesmith

$ heroku config
GITHUB_USERNAME: joesmith
OTHER_VAR:       production

$ heroku config:get GITHUB_USERNAME
joesmith

$ heroku config:remove GITHUB_USERNAME
Unsetting GITHUB_USERNAME and restarting myapp... done, v13

Useful links

active_admin翻译不完全的解决方案

虽然在这里有些翻译

https://github.com/gregbell/active_admin/tree/master/lib/active_admin/locales

但不得不处理依赖devise的部分

$ cd config/locales && wget https://raw.github.com/diaspora/diaspora/master/config/locales/devise/devise.en.yml

$ wget https://raw.github.com/diaspora/diaspora/master/config/locales/devise/devise.ja.yml

参考链接

active_admin 和 will_paginate 使用时产生的错误

在默认情况下,使用这两个gem,运行时会产生如下错误

# http://0.0.0.0:3000/admin/admin_users
undefined local variable or method `per' for []

解决方法是建立这个文件

# config/initializers/will_paginate.rb
if defined?(WillPaginate)
  module WillPaginate
    module ActiveRecord
      module RelationMethods
        alias_method :per, :per_page
        alias_method :num_pages, :total_pages
        alias_method :total_count, :count
      end
    end
  end
end

或者建立这个文件

# config/initializers/kaminari.rb
Kaminari.configure do |config|
  config.page_method_name = :per_page_kaminari
end

参考链接