32

我有一个模型函数,我想确保它使用事务。例如:

class Model 
  def method
    Model.transaction do
      # do stuff
    end
  end
end

我目前的方法是在块内存根方法调用以引发ActiveRecord::Rollback异常,然后检查数据库是否确实发生了变化。但这意味着如果由于某种原因块内的实现发生了变化,那么测试就会中断。

你将如何测试这个?

4

7 回答 7

39

你应该从不同的角度看待问题。从行为的角度来看,测试一个函数是否使用事务是没有用的。它不会为您提供有关函数是否按预期运行的任何信息。

您应该测试的是行为,即预期结果是否正确。为清楚起见,假设您在函数中执行操作 A 和操作 B(在一个事务中执行)。操作 A 在您的应用中向用户赠送 100 美元。操作 B 从用户信用卡中借记 100 美元。

您现在应该为测试提供无效的输入信息,以便从用户信用卡中扣款失败。将整个函数调用包装在一个expect { ... }.not_to change(User, :balance).

这样,您可以测试预期的行为 - 如果信用卡借记失败,请不要将金额记入用户。此外,如果您只是重构代码(例如,您停止使用事务并手动回滚),那么您的测试用例的结果应该不会受到影响。

话虽如此,您仍然应该像@luacassus 提到的那样单独测试这两个操作。此外,如果您对@rb512 提到的源代码进行“不兼容”更改(即更改行为),您的测试用例应该失败是完全正确的。

于 2012-05-29T13:46:43.290 回答
17

需要提到一个大问题:测试事务时,需要关闭 transactional_fixtures。这是因为测试框架(例如 Rspec)将测试用例包装在事务块中。after_commit 永远不会被调用,因为没有真正提交。即使您使用 :requires_new => true,期望事务内部的回滚也不起作用。相反,事务会在测试运行后回滚。参考http://api.rubyonrails.org/classes/ActiveRecord/Transactions/ClassMethods.html嵌套事务。

于 2012-12-28T21:18:54.600 回答
6

通常,您应该使用“纯”rspec 测试来隔离地测试应用程序块(类和方法)。例如,如果您有以下代码:

class Model 
  def method
   Model.transaction do
     first_operation
     second_operation
   end
end

您应该在单独的测试场景中进行测试first_operationsecond_operation最好不要访问数据库。Model#method稍后您可以为这两种方法编写测试并模拟。

在下一步中,您可以使用https://www.relishapp.com/rspec/rspec-rails/docs/request-specs/request-spec编写高级集成测试,并检查此代码在不同条件下将如何影响数据库,例如何时second_method会失败。

在我看来,这是测试产生复杂数据库查询的代码的最实用的方法。

于 2012-05-26T08:18:53.200 回答
2

首先,您需要用块包围您Model.transaction do ... endbegin rescue end块。

检查事务回滚的唯一方法是引发异常。所以你目前的方法都很好。至于您所关心的,实现的改变总是意味着相应地改变测试用例。我认为不可能有一个通用的单元测试用例,即使方法实现发生变化也不需要更改。

我希望这会有所帮助!

于 2012-05-25T10:10:24.033 回答
1

我一直在做同样的事情,但现在我认为您可能需要做的就是测试一个规范中的模型是否调用了“事务”方法,然后在其他单独的规范中测试块的主体。尽管这不能确保事务像当前测试那样包装您的方法调用,而不是其中可能包含的其他代码。

于 2012-05-26T21:29:19.890 回答
0

给定这样的方法:

class ApplicationJob < ActiveJob::Base 
  private

  def transaction(&block)
    # yield
    ActiveRecord::Base.transaction(&block)
  end
end

在规范中调用私有方法,传递一个更改数据库然后引发的块:

RSpec.describe ApplicationJob do
  describe 'helper methods' do
    subject(:job) { Class.new(described_class).new }

    describe '#transaction' do
      it 'runs block in transaction' do
        block = lambda { FactoryBot.create(:user); raise; }
        expect { job.send(:transaction, &block) rescue nil }.not_to change(User, :count)
      end
    end
  end
end

如果您将transaction方法更改为 justyield您将看到即使在提升后数据库也发生了变化。

于 2021-11-30T16:51:45.957 回答
-6

尝试在 Rails 控制台沙盒模式下进行测试

导轨控制台--沙盒

于 2012-06-01T07:33:52.417 回答