Techioz Blog

ロック機構をテストする方法

概要

BankAccountTransaction を BankAccount にインポートするコードがあります

 bank_account.with_lock do
   transactions.each do |transaction|
     import(bank_account, transaction)
   end
 end

正常に動作しますが、トランザクションを 2 回インポートしていないことを 100% 確信できるように、RSpec ケースを書き込む必要があります。

次のヘルパーを書きました

module ConcurrencyHelper
  def make_concurrent_calls(function, concurrent_calls: 2)
    threads = Array.new(concurrent_calls) do
      thread = Thread.new { function.call }
      thread.abort_on_exception = true

      thread
    end

    threads.each(&:join)
  end
end

そして私はそれをRSpecで呼び出しています

context 'when importing the same transaction twice' do
  subject(:concurrent_calls) { make_concurrent_calls(operation) }

  let!(:operation) { -> { described_class.call(params) } }
  let(:filename) { 'single-transaction-response.xml' }

  it 'creates only one transaction' do
    expect { concurrent_calls }.to change(BankaccountTransaction, :count).by(1)
  end
end

しかし何も起こりません。テスト スーツはこの時点でスタックし、エラーなどはスローされません。

スレッドをインスタンス化した直後にデバッグ ポイント (byebug) を配置し、関数を呼び出そうとしましたが、正常に実行されますが、スレッドに参加しても何も起こりません。

これまでに試したこと

他に何かアイデアはありますか?

編集

これが私の現在の DatabaseCleaner 構成です

RSpec.configure do |config|
  config.before(:suite) do
    DatabaseCleaner.clean_with(:deletion)
  end

  config.before do
    DatabaseCleaner.strategy = :transaction
  end

  config.before(:each, js: true) do
    DatabaseCleaner.strategy = :deletion
  end

  config.before do
    DatabaseCleaner.start
  end

  config.after do
    DatabaseCleaner.clean
  end
end

まだ戦略を :deleteion に変更しようとしていません。同様に変更してみます

解決策

with_lock は Rails の実装であり、テストする必要はありません。モックを使用して、コードが with_lock を呼び出すかどうかを確認できます。ここでの唯一のトリックは、トランザクションが確実にインポートされるようにすることです (つまり、with_lock 内のコードが実行されます)。 RSpec は、呼び出すことができるブロックを提供します。以下はその方法の抜粋です。完全に動作する実装はここにあります。

describe "#import_transactions" do
  it "runs with lock" do
    # Test if with_lock is getting called
    expect(subject).to receive(:with_lock) do |*_args, &block|
      # block is provided to with_lock method
      # execute the block and test if it creates transactions
      expect { block.call }
        .to change { BankAccountTransaction.count }.from(0).to(2)
    end

    ImportService.new.import_transactions(subject, transactions)
  end
end