rails中使用RSpec BDD测试入门

Posted by wxianfeng Tue, 02 Feb 2010 21:51:00 GMT


环境 : ruby 1.8.7 + rails 2.3.2



这一demo介绍rspec在rails中的入门 , 需要安装的依赖库 , 和执行过程 , 目录结构等

1, 安装依赖包 rspec 和 rspec-rails

>sudo  gem install rspec
>sudo gem install rspec-rails


安装结束后,>gem list r 查看r开头的gem包:

rspec (1.3.0)
rspec-rails (1.3.2)


rspec 包 是rspec的核心库 , rspec-rails 是把rspec集成到rails中 , 例如 支架 rspec_scaffold , rspec_model 等等

2,新建rails工程

>rails test_rspec -d mysql


3,修改 database.yml

注意 development 和 test 都要修改

4,建立数据库

同时建立 development 和 test 数据库

例如分别为 : test_rspec_development 和 test_rspec_test

5,支架生成MVC和rspec测试文件

>./script/generate rspec_scaffold user name:string


使用rspec_scaffold 比原来的 scaffold 支架 仅仅是多了 rspec部分, 在rails project 中在 spec 文件夹下

6, migrate

>rake db:migrate


6,测试

>rake spec


更多的关于spec的rake可以这样看下:

>rake -T spec


或者

>spec spec/models/user_spec.rb


直接执行某个 rspec 文件 测试 , 使用 spec 执行, 还可以加入一些参数 例如 :

>spec spec/models/user_spec.rb -f specdoc 


输出 doc, 更多参数 >spec -h 查看

ref:

http://wenke.javaeye.com/blog/254496


rails comet juggernaut

Posted by wxianfeng Tue, 24 Nov 2009 03:34:00 GMT
环境 : ruby 1.8.7 + rails 2.1.0 + ubuntu 8.10 desktop

comet 是 server push 技术,说白了就是 服务器端直接 推送数据到客户端,据说 是 未来能取代ajax的一门技术,comet要求客户端 和 server 要建立一个长连接,无论是http 方式 还是 socket 方式,ajax是单用户的异步请求,comet是多用户异步请求,这个技术 已经应用的很广了,特别对于一些 交互性 和 实时性 要求比较高的 系统,例如 股票 实时刷新,web聊天(web qq,gtalk,meebo.com等等),实时提醒(xiaonei.com) 等功能,

那么 在rails中 有这样的comet现成东西吗? 答案是有,juggernaut 插件,juggernaut 原理是什么呢,官网是这样 说的:

1,客户端A 和 comet server 建立一个 socket 连接 (注意这里的server 是 juggernaut的 server)

2,客户端B 向 rails server 发送一个 异步 ajax 请求 (例如这里的rails server 开发环境下为 webrick)

3,rails server 发送数据 给comet server

4,comet server 广播数据到 客户端 (客户端用户可以指定)

plugin 依赖的 库 :

* Rails 2.0.2 or edge

* json gem (gem install json)

* EventMachine gem (gem install eventmachine)

* juggernaut gem (gem install juggernaut)

demo: 实时聊天室

基于 prototype || jQuery

1,新建 rails project

2,安装插件

script/plugin install http://juggernaut.rubyforge.org/svn/trunk/juggernaut


安装完后 会在 config 下看到 juggernaut 的 host 文件

3,配置 host 文件

:hosts:
  - :port: 5001 # 默认就是5001
    :host: 192.168.1.3 # 你电脑的ip
    :public_host: 192.168.1.3
    :public_port: 5001


4,cd 到 config 目录下,生成 juggernaut 的 配置文件

juggernaut -g juggernaut.yml


会在 config下 看到 juggernaut.yml 文件

4,配置 juggernaut.yml

     :allowed_ips: 
                  - 127.0.0.1
                  - 192.168.1.3 # 添加你电脑的ip


5,开启 juggernaut

juggernaut -c juggernaut.yml


显示成 Starting Juggernaut server 0.5.8 on port: 5001... 这样就已经开启了,不要以为还在 start 当中

6,controller

class ChatController < ApplicationController
  
  def index
  end

  def chat_prototype    
  end

  def chat_jquery    
  end

  # prototype
  def send_data_p
    render :juggernaut do |page|
      page.insert_html :top, 'chat_data', "<li>#{h params[:chat_input]}</li>"
    end
    render :nothing => true
  end

  # jQuery
  def send_data_j
    render :juggernaut do |page|
      page["#chat_data"].prepend "<li>#{h params[:chat_input]}</li>"
      # page["#titl"].Text = "wang"
      page["#updatetitle"].click()
    end
    render :nothing => true
  end  
end


7,View

chat_prototype.html.erb (基于 prototype )

<html>
<head>
  <title>测试</title>
  <%= javascript_include_tag :defaults, :juggernaut %>
  <%= juggernaut(:debug => false) %>
</head>
<body>
  <%= form_remote_tag(
    :url => { :action => :send_data_p },
    :complete => "$('chat_input').value = ''" ) %>
  <%= text_field_tag( 'chat_input', '', { :size => 20, :id => 'chat_input'} ) %>
  <%= submit_tag "Add" %>
  </form>
  <ul id="chat_data" style="list-style:none">
  </ul>
</body>
</html>


上面的 <%= juggernaut(:debug => false) %> 就是 和 comet server 建立连接的,还有很多参数可以添加,详细到 juggernaut helper 文件中查看,例如我上面加了 :debug => false 就是 不输出 调试

chat_jquery.html.erb (基于jQuery,这个我给它加了动态改变title 的做法,原先想在action中改的,可是没成功,后来就在action中click页面button,然后button 触发js 修改title 成功)

<html>
  <head>
    <title id="titl" runat="server">测试</title>
    <%= javascript_include_tag 'jquery', 'json','juggernaut/juggernaut', 'juggernaut/jquerynaut', 'juggernaut/swfobject'  %>
    <%= juggernaut(:debug => false) %>
  </head>
  <body>
    <form action="" method="get">
      <div style="margin:0;padding:0">
      <input id="chat_input" name="chat_input" size="20" type="text" value="" />
      <input name="commit" type="submit" value="Add" />
    </form>
    
    <script>
      $(document).ready(function(){

        $('form').submit(function(){
          $.get('/chat/send_data_j', { chat_input: $('#chat_input').val() } )
          return false;
        })
      })

      function settitle() {
        var b = "新提醒";
        var c = ":^:";
        var t = new Date();
        s = t.getSeconds();
        if(s%2 == 0){
          document.title = b;
        }else{
          document.title = c;
        }
        setTimeout("settitle()", 1000);
      }

    </script>

    <div style="display:none">
      <input type='button' id="updatetitle" onclick='settitle();' value='Change Title'/>
    </div>

    <ul id="chat_data" style="list-style:none"></ul>

  </body>
</html>


注意jquery 加载的资源文件,和 prototype 的 不一样,所有资源在下载的 插件media中 可以找到

8,开启webrick

9,打开网址开始聊天

http://192.168.1.3:3000/chat/chat_jquery

多个用户 打开上面的网址就可以实时聊天了.......

ref:

http://juggernaut.rubyforge.org/

http://ajaxian.com/archives/juggernaut-comet-for-rails

http://macrochen.javaeye.com/blog/28020


Rails in place edit with jQuery

Posted by wxianfeng Tue, 13 Oct 2009 16:43:00 GMT
环境 : ruby 1.8.6 + rails 2.1.0 + jquery 1.3.2

效果:

http://www.appelsiini.net/projects/jeditable/default.html

http://15daysofjquery.com/examples/jqueryEditInPlace/divEdit.php

像上面的这种 in place edit 的效果在网络上很常见。。下面介绍一种在rails中的解决方案。。当然后台不一定是 ror 平台了

资源下载:

http://github.com/tuupola/jquery_jeditable 下载后,只需要里面的 jquery.jeditable.js 步骤:

1,加载 jquery.js 和 jquery.jeditable.js

2,View

<script>
  $(document).ready(function() {
    $(".edit_descrip").editable(this.href, {
      indicator : "<img src='/images/indicator.gif'>",
      type   : 'textarea',
      submitdata: { _method: "get" },
      select : true,
      submit : '修改',
      cancel : '取消',
      cssclass : "editable",
      tooltip   : '点击修改',
      id   : 'flag'
    });
  });
</script>
<p  class="edit_descrip" id="edit_descrip" onmouseover="this.style.backgroundColor='yellow'" onmouseout="this.style.backgroundColor='white'"><%= h @client_info.description %> </p>
默认向后台发送的是id 和 value 两个参数,值分别是p对应的id 和 p对应的内容,当然你也可以改变参数名字
id : 'flag'
value : 'newvalue'
就对应传递 flag 和 newvalue 两个参数 了

3,Controller:
if request.xhr?
  if params[:flag]       
        case params[:flag]
        when 'edit_descrip'   
          ClientInfo.update_field(current_client_info.id,params[:value],'descrip') # 更新数据库
          render :text => params[:value] #返回数据
    end
  end
end
4,改变一些默认的参数,可以到 jquery.jeditable.js 中修改,例如 textarea 的默认大小 等等。。。。

5,或者你可以指定target为某个函数,这样就可以自己扩展,例如有额外的返回数据,返回后需要更新某个 DOM元素等等

 <script>
    $(document).ready(function() {
      $(".edit_state").editable(submitEdit, {
        indicator : "<img src='/images/indicator.gif'>",
        type   : 'textarea',
        submitdata: { _method: "get" },
        select : true,
        submit : '修改',
        cancel : '取消',
        cssclass : "editable",
        tooltip   : '点击修改',
        id   : 'flag'
      });
    });

    function submitEdit(value, settings)
    {
      var edits = new Object();
      edits[settings.name] = [value];
      edits[settings.id] = ['edit_state']
      $.ajax({
        url: this.href,
        type: "GET",
        data : edits,
        dataType : "json",
        complete : function ()
        {
          $("#state_time").html('0分钟前');
        }
      });
      return value;
    }

  </script>
See:

http://www.appelsiini.net/projects/jeditable

http://stackoverflow.com/questions/966539/how-to-get-out-of-jquery-jeditable-mess


Rails 自引用实现最近来访

Posted by wxianfeng Mon, 12 Oct 2009 04:33:00 GMT

环境 : ruby 1.8.6 + rails 2.1.0 + ubuntu 8.10

效果:就像 xiaonei 这样

snapshot30

当然最简单的办法 你可以 直接sql操作就ok 了。。。我这里介绍 利用 rails的 activerecord自引用 解决,一个系统有一个users表,但是每一个user可能有friends 或者 followers , 而这个 friends 和 followers 也在 users这个表里,这样就需要 用到 rails的自引用了。

实现最近来访 只是实现了一层的 相当于 取出 twitter的 followers

表结构:

1,用户表 client_infos (username,password,........)

2,访问关系表 vistorships

snapshot31

model关系

class ClientInfo < ActiveRecord::Base
 has_many :vistorships  
 has_many :vistors, :through => :vistorships
end
class Vistorship < ActiveRecord::Base 
 belongs_to :client_info   
belongs_to :vistor, :class_name => "ClientInfo"
end

添加最近来访 数据:

    @client_info = ClientInfo.find(params[:id])
    @recentvistor = @client_info.vistorships.build( :vistor => current_client_info)
    @recentvistor.save

取出全部来访 数据

@client_info.vistorships

Model 取出最近来访的12个vistor

    def get_vistors(client_info) # client_info 是显示那个人的对象
      find(:all,
        :conditions => ["client_info_id = ? and vistor_id <> ?" , client_info.id , client_info.id],
        :select => ["distinct(vistor_id)"],
        :limit => 12,
        :order => 'created_at DESC'
      )
    end

controller中取出数据

@latestvistors = Vistorship.get_vistors(@client_info)

View中输出

<ul>
        <%- for lvistor in @latestvistors -%>
<li>
            <%= image_link lvistor.vistor, :image => :thumbnail , :class => 'image' %>
            <%= client_info_link lvistor.vistor, :class => 'name' %>
          </li>

        <%- end -%>
      </ul>

ref:

http://railscasts.com/episodes/163-self-referential-association


rails send mail set from name

Posted by wxianfeng Thu, 17 Sep 2009 17:14:00 GMT

效果

以前 用 google 的 smtp 服务 成功发送了 email.

http://www.blogjava.net/fl1429/archive/2009/05/04/268866.html

由于 google 的 smtp 每天只容许 发送 500 封email,所以必须自己搭建邮件服务器了。。。。邮件服务器用的是 kerio mail server , 搭好之后 enviroment.rb 中 配置

ActionMailer::Base.perform_deliveries = true
 ActionMailer::Base.raise_delivery_errors = true
 ActionMailer::Base.default_charset = "utf-8"
 ActionMailer::Base.default_content_type = "text/html"
 ActionMailer::Base.smtp_settings = {
     :address => "59.314.13.266",
     :port => 25,
     :domain => "www.abc.com",
     :user_name => "webmaster",
     :password => "riskfit654321",
     :authentication => :login
     }

Model里的send 方法

  def send_password(recipient, subject, name,password)
    @subject = subject 
    @recipients = recipient 
    @from = 'webmaster@abc.com'
    @sent_on = Time.now   
    @body["name"] = name 
    @body['password'] = password 
    @headers = {}
  end

可是 发送 email 后 , 邮箱 显示的发件人 始终是 webmaster , 那么 我 想让发件人的名字显示成 测试 , 该如何设置呢,那么把@from 设置成这样 :

@from = '"测试"<webmaster@abc.com>' # 主意是外面是单引号 , 里面 是 双引号 ,而且用户名webmaster 必须和 config里的user_name 一样

ref:

http://www.javaeye.com/topic/126875


Rails 动态生成表和Model

Posted by wxianfeng Sat, 01 Aug 2009 17:12:00 GMT

环境:ruby 1.9.2 + rails 3.0.3 + ubuntu 10.10

项目需要运行中动态生成表 和 Model , 怎么办 ?

借助 ActiveRecord::Migration 来实现 动态建表 和 字段

可以 借助 Object.const_set 来实现 动态Model

DEMO:

# RUN : rails runner lib/dynamic_table.rb

ActiveRecord::Migration.create_table :posts
ActiveRecord::Migration.add_column :posts, :title, :string

Object.const_set(:Post,Class.new(ActiveRecord::Base)) # => Object.class_eval { const_set(:Post,Class.new(ActiveRecord::Base)) }
# p Post.columns
p Post.column_names # ["id", "title"]

ActiveRecord::Migration.add_column :posts, :body, :text

p Post.column_names # ["id", "title"]

Object.class_eval { remove_const :Post }
Object.const_set(:Post,Class.new(ActiveRecord::Base))

p Post.column_names # ["id", "title", "body"]

动态Model 实质就相当于 Post = Class.new(ActiveRecord::Base) 或者 Post < ActiveRecord::Base

Class.new(ActiveRecord::Base) 参数指定 super_class , 默认是 Object

可以从ruby源码中看出:

  #     Class.new(super_class=Object)   =>    a_class
  #
  #
  # Creates a new anonymous (unnamed) class with the given superclass
  # (or <code>Object</code> if no parameter is given). You can give a
  # class a name by assigning the class object to a constant.
  #
  #
  #
  def self.new(super_class=Object)
    # This is just a stub for a builtin Ruby method.
    # See the top of this file for more info.
  end

当给表添加了新的字段后,Model 需要重新 const_set 一次 ,注意 const_set 之前 需要 remove_const 一次 , 不然会出现 已经初始化的警告

see:

http://hildolfur.wordpress.com/2006/10/29/class-reloading-in-ruby/


rails check_box_tag boolean

Posted by wxianfeng Tue, 30 Jun 2009 16:16:00 GMT

环境:ruby 1.9.2 + rails 3.0.3 + ubuntu 10.10

scaffold 生成的 form_for 对应的 boolean 类型 是check_box , 数据库中 值是 1 check_box就选中, 是 0 就不选中, 但是不是 form_for 的话 ,check_box_tag 怎么实现呢

仿照 form_for check_box 生成的代码:

<input name="user[abc]" type="hidden" value="0" />
<input checked="checked" id="user_abc" name="user[abc]" type="checkbox" value="1" />

可以看出 多生成了 一段 hidden , 为什么 ?

因为 check_box 的工作原理是 :

选中了 就传check_box 的参数 , 不选中 不传 check_box 的 参数

所以上面 选中的话,传的值是 1

没选中的话, 传的是 0

所以check_box_tag 的话,就应该这样写了.

     <%= hidden_field_tag "user[abc]" , 0 %>
      <%= check_box_tag "user[abc]" , 1 , (true if ele.abc == 1)  %>

注意 需要 添加 hidden 域 , 并且 需要在 checkbox 的上面


netbeans debug rails 3.0

Posted by wxianfeng Sun, 31 May 2009 22:09:00 GMT

环境:ruby 1.9.2 + rails 3.0.3 + nb 6.9.1 + ubuntu 10.10

nb debug rails 3.0 还没有跟上,不可以debug rails 3.0项目了,网上也有很多人遇到,可能出现下面这个异常:

Uncaught exception: no such file to load -- script/rails

我有一个解决办法,把你要debug的项目path直接写进去,

需要用到的gem:

ruby-debug-base19 (0.11.24)
ruby-debug-ide (0.4.9)
linecache19 (0.5.11)

如果是debug test file 的话,还需要 test-unit

wxianfeng@ubuntu:~$ gem list ruby-

*** LOCAL GEMS ***

ruby-debug-base19 (0.11.24)
ruby-debug-ide (0.4.9)
wxianfeng@ubuntu:~$ gem list line

*** LOCAL GEMS ***

linecache19 (0.5.11)
wxianfeng@ubuntu:~$ gem list test

*** LOCAL GEMS ***

test-unit (2.1.2)

当前rvm环境;

wxianfeng@ubuntu:~$ rvm info

ruby-1.9.2-p0:

  system:
    uname:       "Linux ubuntu 2.6.35-22-generic #33-Ubuntu SMP Sun Sep 19 20:34:50 UTC 2010 i686 GNU/Linux"
    bash:        "/bin/bash => GNU bash, version 4.1.5(1)-release (i686-pc-linux-gnu)"
    zsh:         " => not installed"

  rvm:
    version:      "rvm 1.1.0 by Wayne E. Seguin (wayneeseguin@gmail.com) [http://rvm.beginrescueend.com/]"

  ruby:
    interpreter:  "ruby"
    version:      "1.9.2p0"
    date:         "2010-08-18"
    platform:     "i686-linux"
    patchlevel:   "2010-08-18 revision 29036"
    full_version: "ruby 1.9.2p0 (2010-08-18 revision 29036) [i686-linux]"

  homes:
    gem:          "/home/wxianfeng/.rvm/gems/ruby-1.9.2-p0"
    ruby:         "/home/wxianfeng/.rvm/rubies/ruby-1.9.2-p0"

  binaries:
    ruby:         "/home/wxianfeng/.rvm/rubies/ruby-1.9.2-p0/bin/ruby"
    irb:          "/home/wxianfeng/.rvm/rubies/ruby-1.9.2-p0/bin/irb"
    gem:          "/home/wxianfeng/.rvm/rubies/ruby-1.9.2-p0/bin/gem"
    rake:         "/home/wxianfeng/.rvm/gems/ruby-1.9.2-p0/bin/rake"

  environment:
    PATH:         "/home/wxianfeng/.rvm/gems/ruby-1.9.2-p0/bin:/home/wxianfeng/.rvm/gems/ruby-1.9.2-p0@global/bin:/home/wxianfeng/.rvm/rubies/ruby-1.9.2-p0/bin:/home/wxianfeng/.rvm/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/system/mysql/bin:/usr/local/system/jdk1.6.0_22/bin"
    GEM_HOME:     "/home/wxianfeng/.rvm/gems/ruby-1.9.2-p0"
    GEM_PATH:     "/home/wxianfeng/.rvm/gems/ruby-1.9.2-p0:/home/wxianfeng/.rvm/gems/ruby-1.9.2-p0@global"
    MY_RUBY_HOME: "/home/wxianfeng/.rvm/rubies/ruby-1.9.2-p0"
    IRBRC:        "/home/wxianfeng/.rvm/rubies/ruby-1.9.2-p0/.irbrc"
    RUBYOPT:      ""
    gemset:       ""

注意把netbeans的 gem_home , gem_path 设置为 当前 rvm 对应的 gem_home , gem_path , 设置gem_path 时 nb 默认 .rvm 目录不显示,需要直接在 对话框中输入路径

最后修改

/home/wxianfeng/.rvm/gems/ruby-1.9.2-p0/gems/ruby-debug-ide-0.4.9/lib/ruby-debug-ide.rb

大概 110 行

      path = "/usr/local/system/projects/entos/ent_os/script/rails"
      bt = debug_load(path, options.stop, options.load_mode)
      #      bt = debug_load(Debugger::PROG_SCRIPT, options.stop, options.load_mode)

注释一行,添加两行

把你需要调试的项目 set as main project ,然后 debug main project !

ok~!

如果你需要debug 单个ruby 文件 , 那么 需要改回来 , 这里注意.

see:
http://forums.netbeans.org/topic31103.html


Rails 测试注意 use_transactional_fixtures

Posted by wxianfeng Tue, 31 Mar 2009 17:57:00 GMT

环境:ruby 1.9.2 + rails 3.0.3 + ubuntu 10.10 + mysql 5.1

这个问题搞了我接近两个工作日,现在还在恼火中,之前大家伙都没遇到过这个问题, 但是又必须要解决,不解决我的测试就跑不起来了,问题是这样的,用rails scaffold 生成的测试代码,

生成代码;

rails new scaffold -d mysql
rake db:create:all
rails g scaffold user name:string password:string birthday_at:datetime
rake db:migrate
rake test

执行rake test 后,出现错误:

ActiveRecord::RecordNotFound: Couldn't find User with ID=980190962

报错的是controller action的测试, 生成fixtures 的代码

users.yml

# Read about fixtures at http://ar.rubyonrails.org/classes/Fixtures.html

one:
  name: MyString
  password: MyString
  birthday_at: 2010-11-29 11:03:20

two:
  name: MyString
  password: MyString
  birthday_at: 2010-11-29 11:03:20

测试代码 userscontrollertest.rb:

# 执行顺序是按照name的排序(ASC)来执行的
require  File.expand_path("./test/test_helper")
# require 'test_helper'

#p Fixtures.identify(:one) # autogenerate id 的算法
#p Fixtures.identify(:two)

class UsersControllerTest < ActionController::TestCase


  setup do # 初始化对象 , 没有load 数据
    @user = users(:one)
  end

  test "should get index" do # 只要 执行了 test 方法,fixtures 的数据就已经load到test数据库中了
    get :index
    assert_response :success
    assert_not_nil assigns(:users)
  end

  test "should get new" do
    get :new
    assert_response :success
  end

  test "should create user" do 
    assert_difference('User.count') do
      post :create, :user => @user.attributes
    end
  
    assert_redirected_to user_path(assigns(:user))
  end

  test "should show user" do
    get :show, :id => @user.to_param
    assert_response :success
  end
  
  test "should get edit" do
    get :edit, :id => @user.to_param
    assert_response :success
  end
  
  test "should update user" do
    put :update, :id => @user.to_param, :user => @user.attributes
    assert_redirected_to user_path(assigns(:user))
  end
  
  test "should destroy user" do
    assert_difference('User.count', -1) do
      delete :destroy, :id => @user.to_param
    end

    assert_redirected_to users_path
  end
end

我想不应该啊,rails 经典的scaffold怎么会报错,于是开始排查: 1,在同事,宿舍,VPS上分别都做了测试 都通过了,都没有问题

2,下一步通过debug跟踪来调:

最后还是没有解决,但是 发现了一点蛛丝马迹,发现了 test 的执行顺序,是按照name的升序来执行的,上面图可以看出,不用netbeans怎么看出来?在Rails.root 根下执行:

yang@yang-OptiPlex-380:/usr/local/system/scaffold$ ruby test/functional/users_controller_test.rb -v
Loaded suite test/functional/users_controller_test
Started
UsersControllerTest#test_should_create_user: 0.19 s: .
UsersControllerTest#test_should_destroy_user: 0.06 s: .
UsersControllerTest#test_should_get_edit: 0.01 s: .
UsersControllerTest#test_should_get_index: 0.01 s: .
UsersControllerTest#test_should_get_new: 0.03 s: .
UsersControllerTest#test_should_show_user: 0.01 s: .
UsersControllerTest#test_should_update_user: 0.01 s: .

Finished in 0.310882 seconds.

7 tests, 10 assertions, 0 failures, 0 errors, 0 skips

Test run options: --seed 52586 --verbose

就是跑单个测试文件,加上 -v 参数 ,更多的可以 -h 查看下,注意把单个文件的 test_helper 路径加入正确,上面的我那个已经修改过来.

还可以看出 是执行到 destroy 方法的时候报错的,把这个destroy方法注释掉,问题就没了,但是根本问题没解决.

3,改变ruby rails 版本测试 切换到ruby 1.8.7 + rails 2.3.5 scaffold生成测试代码,问题依然存在

4,跟踪测试log看看

SQL (0.0ms)  BEGIN
  SQL (0.2ms)  SHOW TABLES
  User Load (0.1ms)  SELECT `users`.* FROM `users` WHERE (`users`.`id` = 980190962) LIMIT 1
  SQL (0.1ms)  SELECT COUNT(*) FROM `users`
  Processing by UsersController#create as HTML
  Parameters: {"user"=>{"birthday_at"=>2010-11-29 11:03:20 UTC, "created_at"=>2010-12-01 07:20:20 UTC, "id"=>980190962, "name"=>"MyString", "password"=>"[FILTERED]", "updated_at"=>2010-12-01 07:20:20 UTC}}
WARNING: Can't mass-assign protected attributes: id
  SQL (0.1ms)  SAVEPOINT active_record_1
  SQL (0.5ms)  describe `users`
  AREL (0.2ms)  INSERT INTO `users` (`name`, `password`, `birthday_at`, `created_at`, `updated_at`) VALUES ('MyString', 'MyString', '2010-11-29 11:03:20', '2010-12-01 07:20:20', '2010-12-01 07:20:20')
  SQL (0.0ms)  RELEASE SAVEPOINT active_record_1
Redirected to http://test.host/users/980190964
Completed 302 Found in 11ms
  SQL (0.1ms)  SELECT COUNT(*) FROM `users`
  SQL (0.0ms)  ROLLBACK
  SQL (0.0ms)  BEGIN
  User Load (0.1ms)  SELECT `users`.* FROM `users` WHERE (`users`.`id` = 980190962) LIMIT 1
  SQL (0.1ms)  SELECT COUNT(*) FROM `users`
  Processing by UsersController#destroy as HTML
  Parameters: {"id"=>"980190962"}
  User Load (0.2ms)  SELECT `users`.* FROM `users` WHERE (`users`.`id` = 980190962) LIMIT 1
  SQL (0.0ms)  SAVEPOINT active_record_1
  AREL (0.1ms)  DELETE FROM `users` WHERE (`users`.`id` = 980190962)
  SQL (0.0ms)  RELEASE SAVEPOINT active_record_1
Redirected to http://test.host/users
Completed 302 Found in 2ms
  SQL (0.1ms)  SELECT COUNT(*) FROM `users`
  SQL (0.0ms)  ROLLBACK
  SQL (0.0ms)  BEGIN
  User Load (0.1ms)  SELECT `users`.* FROM `users` WHERE (`users`.`id` = 980190962) LIMIT 1
  SQL (0.0ms)  ROLLBACK
  SQL (0.0ms)  BEGIN
  User Load (0.1ms)  SELECT `users`.* FROM `users` WHERE (`users`.`id` = 980190962) LIMIT 1
  SQL (0.0ms)  ROLLBACK
  SQL (0.0ms)  BEGIN
  User Load (0.1ms)  SELECT `users`.* FROM `users` WHERE (`users`.`id` = 980190962) LIMIT 1
  SQL (0.0ms)  ROLLBACK
  SQL (0.0ms)  BEGIN
  User Load (0.1ms)  SELECT `users`.* FROM `users` WHERE (`users`.`id` = 980190962) LIMIT 1
  SQL (0.0ms)  ROLLBACK
  SQL (0.0ms)  BEGIN
  User Load (0.1ms)  SELECT `users`.* FROM `users` WHERE (`users`.`id` = 980190962) LIMIT 1
  SQL (0.0ms)  ROLLBACK

这是test输出的log,发现了好东西,ROLLBACk ,这个问题就来了,经过进一步的研究,我把 destroy 的测试方法给注释了,跑了我的测试 ,最后数据库中产生 3条数据,但是正确的一台机子上 最后数据库中users表中是 2 条数据,问题就在这里 ,最后搞懂了 rails 自带testcase的测试原理,才恍然大悟!

测试的表必须 innodb 引擎,因为测试的原理默认是 事务回滚 机制,来看看测试代码就知道了:

test "should create user" do 
    assert_difference('User.count') do
      post :create, :user => @user.attributes
    end
  
    assert_redirected_to user_path(assigns(:user))
  end

这段代码 的意思是 测试前的 User.count + 1 = yield block后的 User.count , 测试就是success了,1是默认值,可以查看assert_difference源码得知, 但是数据是回滚的,例如测试前数据库中是两条数据,那么测试create数据后,数据库中应该还是原来的fixtures中的两条数据

同理 destroy 的原理一样;

 test "should destroy user" do

assert_difference(<span class="s"><span class="dl">'</span><span class="k">User.count</span><span class="dl">'</span></span>, <span class="i">-1</span>) <span class="r">do</span>
  delete <span class="sy">:destroy</span>, <span class="sy">:id</span> =&gt; <span class="iv">@user</span>.to_param
<span class="r">end</span>

assert_redirected_to users_path

end

看下 assert_difference 源码(重点看注释):

     # Test numeric difference between the return value of an expression as a result of what is evaluated
      # in the yielded block.
      #
      #   assert_difference 'Article.count' do
      #     post :create, :article => {...}
      #   end
      #
      # An arbitrary expression is passed in and evaluated.
      #
      #   assert_difference 'assigns(:article).comments(:reload).size' do
      #     post :create, :comment => {...}
      #   end
      #
      # An arbitrary positive or negative difference can be specified. The default is +1.
      #
      #   assert_difference 'Article.count', -1 do
      #     post :delete, :id => ...
      #   end
      #
      # An array of expressions can also be passed in and evaluated.
      #
      #   assert_difference [ 'Article.count', 'Post.count' ], +2 do
      #     post :create, :article => {...}
      #   end
      #
      # A error message can be specified.
      #
      #   assert_difference 'Article.count', -1, "An Article should be destroyed" do
      #     post :delete, :id => ...
      #   end
      def assert_difference(expression, difference = 1, message = nil, &block)
        b = block.send(:binding)
        exps = Array.wrap(expression)
        before = exps.map { |e| eval(e, b) }
        p before

        yield

        exps.each_with_index do |e, i|
          error = "#{e.inspect} didn't change by #{difference}"
          error = "#{message}.\n#{error}" if message
          assert_equal(before[i] + difference, eval(e, b), error)
        end
      end

我的mysql是 myisam 引擎,test.log 的insert那一部分输出ROLLBACK,其实没有rollback,表中insert 数据进去了,所以这里的log输出是错误的,谴责下DHH,开玩笑了^_^.

期间遇到的问题;

1,fixtures现在不用指定id,会自己给你生成,那生成的算法是什么呢?

Fixtures.identify(:one)

demo:

wxianfeng@ubuntu:/usr/local/system/projects/rails3__scaffold$ rails c
Loading development environment (Rails 3.0.3)
ruby-1.9.2-p0 > require "rails/test_help"
 => ["FixtureClassNotFound", "FixturesFileNotFound", "Fixtures", "Fixture"] 
ruby-1.9.2-p0 > Fixtures.identify(:one)
 => 980190962 

看下 identify 方法的源码:

MAX_ID = 2 ** 30 - 1

  def self.identify(label)
    Zlib.crc32(label.to_s) % MAX_ID
  end

2,怎么在netbeans中debug测试文件呢,即上面的图

需要安装 ruby-debug-ide , ruby-debug-base , test-unit , 还需要 把依赖的 gem包都打进入项目中,

bundle install .  # . 是path.当前位置

即可,你的Rails.root下多了ruby文件夹,里面都是用到的 gems ,

3,怎么跑单个测试文件?

ruby test/functional/users_controller_test.rb # 注意修改 require test_helper ,默认会找不到

4,怎么测试一个测试文件中的某个测试方法?

ruby test/functional/users_controller_test.rb -n test_should_show_user

5,既然测试是事务安全型的 ,那我可以把事务关闭测试吗 可以,添加

self.use_transactional_fixtures = false 即可

6,测试的时候 fixtures.yml 中数据 什么时候 被load到表中的呢

结果测试 只需要有test 定义就可以,哪怕

test ""  do
end

block中没有任何测试的东西,这个时候 数据已经 load 的数据库中

7, 用 rspec 有没有这个问题 同理, 一样存在这个问题

建议:

mysql 数据库表建议都是innodb引擎,innodb 确实比myisam有优势,后期的mysql版本默认用的都是innodb了,之前都是myisam默认的,上次编译安装mysql忘记把innodb编译进去了,需要重新编译,所以也才造就了这次的一系列折腾!

疑问:

rails 自带的测试 必须 innodb引擎吗?google 了下,毛也搜不到, here 但是实验下来确实是这样的.很是疑问

再经过探究,test 可以设置 事务型的和 非事务型的,innodb 两者都可以,myisam 就必须是 非事务型的了

class UsersControllerTest < ActionController::TestCase
  self.use_transactional_fixtures = false
end

ref: http://stackoverflow.com/questions/2616173/rails-unit-testing-with-myisam-tables http://guides.rubyonrails.org/testing.html http://ar.rubyonrails.org/classes/Fixtures.html http://stackoverflow.com/questions/763881/automatic-associations-in-ruby-on-rails-fixtures http://railstips.org/blog/archives/2006/06/30/assert_difference-for-easier-tests/ http://www.softiesonrails.com/2007/3/7/how-to-run-a-single-test-from-the-command-line http://www.javaeye.com/wiki/Rails-EveryDay/1047-Rails宝典八十一式:Rails2.0之Fixtures尝鲜


rails 脚本里调用helper方法

Posted by wxianfeng Sun, 01 Feb 2009 14:21:00 GMT

环境:ubuntu 10.10 + ruby 1.8.7 + rails 2.3.5

有这样一个需求,以前写script都是一般调用Model里的方法,这次不同,调用的是helper方法,需要批量的更新数据库中的一个字段,于是写了个script,然后用script/runner执行,需要调用一个helper方法,出现下面了这个错误:

NoMethodError: undefined method `url_for' for nil:NilClass

最后发现原因是 helper 方法里有 link_to 导致的,那怎么办?也不好调试,runner输出的玩意也跟踪不到错误的地方,只是抛出异常,于是通过我不断的debug到rails源码里,查出到底谁是nil了,最终找到了禍手,并且现在可以很方便的在script里用helper任何方法了,下面我把过程重现一遍.

建好demo project:

rails scaffold -d mysql
cd scaffold
rake db:create
ruby script/generate scaffold user name:string password:string birthday_at:datetime
rake db:migrate
rake rails:freeze:gems  # 把rails打包进去 ,为了方便查看rails源码和debug
ruby script/server
create 一条user数据

users_helper.rb 添加如下代码:

  def link_to_user
   # link_to "user_first","/users/show/#{User.first.id}"  这个不会报错,why? 后面会提到
    link_to "user_first",:controller => "users",:action=>"show",:id=>User.first
  end

script 代码: RAILSROOT/script/tools/callhelper.rb:

# at RAILS_ROOT run : ruby script/runner script/tools/call_helper.rb
p ApplicationController.helpers.link_to_user

调用 runner执行报错:

NoMethodError: undefined method `url_for' for nil:NilClass

debug 跟进去发现是link_to 的问题:

linkto 源码:(/usr/local/system/projects/scaffold/vendor/rails/actionpack/lib/actionview/helpers/url_helper.rb)

def link_to(*args, &block)

    <span class="r">if</span> block_given?
      options      = args.first || {}
      html_options = args.second
      concat(link_to(capture(&amp;block), options, html_options).html_safe!)
    <span class="r">else</span>
      name         = args.first
      options      = args.second || {}
      html_options = args.third

      url = url_for(options)

      <span class="r">if</span> html_options
        html_options = html_options.stringify_keys
        href = html_options[<span class="s"><span class="dl">'</span><span class="k">href</span><span class="dl">'</span></span>]
        convert_options_to_javascript!(html_options, url)
        tag_options = tag_options(html_options)
      <span class="r">else</span>
        tag_options = <span class="pc">nil</span>
      <span class="r">end</span>

      href_attr = <span class="s"><span class="dl">&quot;</span><span class="k">href=</span><span class="ch">\&quot;</span><span class="il"><span class="idl">#{</span>url<span class="idl">}</span></span><span class="ch">\&quot;</span><span class="dl">&quot;</span></span> <span class="r">unless</span> href
      <span class="s"><span class="dl">&quot;</span><span class="k">&lt;a </span><span class="il"><span class="idl">#{</span>href_attr<span class="idl">}</span></span><span class="il"><span class="idl">#{</span>tag_options<span class="idl">}</span></span><span class="k">&gt;</span><span class="il"><span class="idl">#{</span>name || url<span class="idl">}</span></span><span class="k">&lt;/a&gt;</span><span class="dl">&quot;</span></span>.html_safe!
    <span class="r">end</span>

end

里面又调用到了 urlfor 方法,urlfor 源码:

def url_for(options = {})

    options ||= {}
    url = <span class="r">case</span> options
    <span class="r">when</span> <span class="co">String</span>
      escape = <span class="pc">true</span>
      options
    <span class="r">when</span> <span class="co">Hash</span>
      options = { <span class="sy">:only_path</span> =&gt; options[<span class="sy">:host</span>].nil? }.update(options.symbolize_keys)
      escape  = options.key?(<span class="sy">:escape</span>) ? options.delete(<span class="sy">:escape</span>) : <span class="pc">true</span>
      <span class="iv">@controller</span>.send(<span class="sy">:url_for</span>, options)
    <span class="r">when</span> <span class="sy">:back</span>
      escape = <span class="pc">false</span>
      <span class="iv">@controller</span>.request.env[<span class="s"><span class="dl">&quot;</span><span class="k">HTTP_REFERER</span><span class="dl">&quot;</span></span>] || <span class="s"><span class="dl">'</span><span class="k">javascript:history.back()</span><span class="dl">'</span></span>
    <span class="r">else</span>
      escape = <span class="pc">false</span>
      polymorphic_path(options)
    <span class="r">end</span>

    escape ? escape_once(url) : url
  <span class="r">end</span></pre></div>

执行的是 Hash 那一块,好,就在这设断点,最后发现了真正的问题 原来是 @controller.send(:urlfor, options) 这里报错的,@controller 为nil,之前为什么 linkto 后面给string不报错,url_for源码已经告诉你了

那么如何解决,修改call_helper.rb:

# at RAILS_ROOT run : ruby script/runner script/tools/call_helper.rb
include ActionController::UrlWriter
p ApplicationController.helpers.link_to_user

ok,这下就可以了.

ActionController::UrlWriter 中url_for 源码;

def url_for(options)

  options = <span class="pc">self</span>.class.default_url_options.merge(options)

  url = <span class="s"><span class="dl">'</span><span class="dl">'</span></span>

  <span class="r">unless</span> options.delete(<span class="sy">:only_path</span>)
    url &lt;&lt; (options.delete(<span class="sy">:protocol</span>) || <span class="s"><span class="dl">'</span><span class="k">http</span><span class="dl">'</span></span>)
    url &lt;&lt; <span class="s"><span class="dl">'</span><span class="k">://</span><span class="dl">'</span></span> <span class="r">unless</span> url.match(<span class="s"><span class="dl">&quot;</span><span class="k">://</span><span class="dl">&quot;</span></span>)

    raise <span class="s"><span class="dl">&quot;</span><span class="k">Missing host to link to! Please provide :host parameter or set default_url_options[:host]</span><span class="dl">&quot;</span></span> <span class="r">unless</span> options[<span class="sy">:host</span>]

    url &lt;&lt; options.delete(<span class="sy">:host</span>)
    url &lt;&lt; <span class="s"><span class="dl">&quot;</span><span class="k">:</span><span class="il"><span class="idl">#{</span>options.delete(<span class="sy">:port</span>)<span class="idl">}</span></span><span class="dl">&quot;</span></span> <span class="r">if</span> options.key?(<span class="sy">:port</span>)
  <span class="r">else</span>
    <span class="c"># Delete the unused options to prevent their appearance in the query string.</span>
    [<span class="sy">:protocol</span>, <span class="sy">:host</span>, <span class="sy">:port</span>, <span class="sy">:skip_relative_url_root</span>].each { |k| options.delete(k) }
  <span class="r">end</span>
  trailing_slash = options.delete(<span class="sy">:trailing_slash</span>) <span class="r">if</span> options.key?(<span class="sy">:trailing_slash</span>)
  url &lt;&lt; <span class="co">ActionController</span>::<span class="co">Base</span>.relative_url_root.to_s <span class="r">unless</span> options[<span class="sy">:skip_relative_url_root</span>]
  anchor = <span class="s"><span class="dl">&quot;</span><span class="k">#</span><span class="il"><span class="idl">#{</span><span class="co">CGI</span>.escape options.delete(<span class="sy">:anchor</span>).to_param.to_s<span class="idl">}</span></span><span class="dl">&quot;</span></span> <span class="r">if</span> options[<span class="sy">:anchor</span>]
  generated = <span class="co">Routing</span>::<span class="co">Routes</span>.generate(options, {})
  url &lt;&lt; (trailing_slash ? generated.sub(<span class="rx"><span class="dl">/</span><span class="ch">\?</span><span class="k">|</span><span class="ch">\z</span><span class="dl">/</span></span>) { <span class="s"><span class="dl">&quot;</span><span class="k">/</span><span class="dl">&quot;</span></span> + <span class="gv">$&amp;</span> } : generated)
  url &lt;&lt; anchor <span class="r">if</span> anchor

  url
<span class="r">end</span>

end

发现这个 urlfor 已经和 actonview 下的不一样了,完全重写了,所以 当你include ActionController::UrlWriter 后,调用的是这个 urlfor 方法,而不是之前的那个 urlfor 了........这两个方法 源码 还有待进一步研究,之前nil问题解决了.

期间遇到的问题:

1,在rails console 里可以直接调用 helper 方法,为什么在用runner执行的script不可以找到helper变量?

console 中helper:

wxianfeng@ubuntu:/usr/local/system/projects/scaffold$ ruby script/console 
Loading development environment (Rails 2.3.5)
ruby-1.8.7-p302 > helper
 => #<ActionView::Base:0xb71dc038 @assigns_added=nil, @_first_render=nil, @assigns={}, @view_paths=[], @helpers=#<ActionView::Base::ProxyModule:0xb71dbf98>, @controller=nil, @_current_render=nil> 

那我们就来看看 runner 和 console 到底有什么区别:

runner源码:

#!/usr/bin/env ruby
require File.expand_path('../../config/boot',  __FILE__)
require 'commands/runner

console源码:

#!/usr/bin/env ruby
require File.expand_path('../../config/boot',  __FILE__)
require 'commands/console'

commands/runner 源码:(/usr/local/system/projects/scaffold/vendor/rails/railties/lib/commands/runner.rb)

require 'optparse'

options = { :environment => (ENV['RAILS_ENV'] || "development").dup }
code_or_file = nil

ARGV.clone.options do |opts|
  script_name = File.basename($0)
  opts.banner = "Usage: #{$0} [options] ('Some.ruby(code)' or a filename)"

  opts.separator ""

  opts.on("-e", "--environment=name", String,
          "Specifies the environment for the runner to operate under (test/development/production).",
          "Default: development") { |v| options[:environment] = v }

  opts.separator ""

  opts.on("-h", "--help",
          "Show this help message.") { $stderr.puts opts; exit }

  if RUBY_PLATFORM !~ /mswin/
    opts.separator ""
    opts.separator "You can also use runner as a shebang line for your scripts like this:"
    opts.separator "-------------------------------------------------------------"
    opts.separator "#!/usr/bin/env #{File.expand_path($0)}"
    opts.separator ""
    opts.separator "Product.find(:all).each { |p| p.price *= 2 ; p.save! }"
    opts.separator "-------------------------------------------------------------"
  end

  opts.order! { |o| code_or_file ||= o } rescue retry
end

ARGV.delete(code_or_file)

ENV["RAILS_ENV"] = options[:environment]
RAILS_ENV.replace(options[:environment]) if defined?(RAILS_ENV)

require RAILS_ROOT + '/config/environment'

begin
  if code_or_file.nil?
    $stderr.puts "Run '#{$0} -h' for help."
    exit 1
  elsif File.exist?(code_or_file)
    eval(File.read(code_or_file), nil, code_or_file)
  else
    eval(code_or_file)
  end
ensure
  if defined? Rails
    Rails.logger.flush if Rails.logger.respond_to?(:flush)
  end
end

commands/console.rb 源码 (/usr/local/system/projects/scaffold/vendor/rails/railties/lib/commands/console.rb)

irb = RUBY_PLATFORM =~ /(:?mswin|mingw)/ ? 'irb.bat' : 'irb'

require 'optparse'

options = { :sandbox => false, :irb => irb }
OptionParser.new do |opt|
  opt.banner = "Usage: console [environment] [options]"
  opt.on('-s', '--sandbox', 'Rollback database modifications on exit.') { |v| options[:sandbox] = v }
  opt.on("--irb=[#{irb}]", 'Invoke a different irb.') { |v| options[:irb] = v }
  opt.on("--debugger", 'Enable ruby-debugging for the console.') { |v| options[:debugger] = v }
  opt.parse!(ARGV)
end

libs =  " -r irb/completion"
libs << %( -r "#{RAILS_ROOT}/config/environment")
libs << " -r console_app"
libs << " -r console_sandbox" if options[:sandbox]
libs << " -r console_with_helpers"

if options[:debugger]
  begin
    require 'ruby-debug'
    libs << " -r ruby-debug"
    puts "=> Debugger enabled"
  rescue Exception
    puts "You need to install ruby-debug to run the console in debugging mode. With gems, use 'gem install ruby-debug'"
    exit
  end
end

ENV['RAILS_ENV'] = case ARGV.first
  when "p"; "production"
  when "d"; "development"
  when "t"; "test"
  else
    ARGV.first || ENV['RAILS_ENV'] || 'development'
end

if options[:sandbox]
  puts "Loading #{ENV['RAILS_ENV']} environment in sandbox (Rails #{Rails.version})"
  puts "Any modifications you make will be rolled back on exit"
else
  puts "Loading #{ENV['RAILS_ENV']} environment (Rails #{Rails.version})"
end
exec "#{options[:irb]} #{libs} --simple-prompt"

可以发现 ,原来 console 里 加载了 consolewithhelpers ,另外 发现 其实 rails console就是 irb 只不过 加载了 一些lib

看看 consolewithhelpers 源码:(/usr/local/system/projects/scaffold/vendor/rails/railties/lib/consolewithhelpers.rb)

def helper
  @helper ||= ApplicationController.helpers
end

@controller = ApplicationController.new

so,看明白了把,改变原来的 script:

# at RAILS_ROOT run : ruby script/runner script/tools/call_helper.rb
include ActionController::UrlWriter
require 'console_with_helpers'
p helper.link_to_user

2,那我直接用ruby调用一个script可以吗,不用runner来执行,可以.

#!/usr/bin/env ruby
#
# author : wang.fl1429@gmail.com
# run at RAILS_ROOT :  ruby script/tools/call_helper_script.rb
require File.expand_path('../../../config/boot',  __FILE__)
# ENV['RAILS_ENV'] ||= 'production'
require RAILS_ROOT + '/config/environment'
include ActionController::UrlWriter
p ApplicationController.helpers.link_to_user

3,为什么一般长久大批量执行用 runner ,而不是直接用 ruby 执行

看上面rails runner.rb的源码 也没发现为什么 ,只是用了eval 来执行ruby代码, 改天 benchmark 一把看看 , 瞧瞧eval 的好处!,不用 rails runner 我们同样可以达到runner的效果:

ruby eval(File.read("/....../call_helper.rb"))

demo project download: here

SEE: http://kpumuk.info/ruby-on-rails/memo-6-using-named-routes-and-url_for-outside-the-controller-in-ruby-on-rails/