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尝鲜