ARは二度死なない

ごきげんよう。
MFクラウド給与の開発を担当している、Railsエンジニアの大津です。
朝ドラに影響されているわけではありません。

先日、モデルのdestroy呼び出しの確認をするために、

expect(tested).to receive(:destroy).once

のようにして、テストを書いたら、以下のように失敗してしまうことがありました。

           expected: 1 time with any arguments
           received: 2 times

 

設計を間違えて、ActiveRecord.destroyを二回やっていた

MFクラウドシリーズには、会計や請求書などの複数のプロダクトがあります。

それらの処理は相互に関わっている部分があり、意図せずモデルのdestroyが2回呼ばれていたことが原因だったのですが、
「ARってdestroyを2回呼んじゃって大丈夫なのか?」という不安が出てきました。

結論としては、「ARが上手いこと判断してくれて大丈夫だった」のですが、サンプルを使って、その挙動を確認してみようかと思います。
 

サンプル用意

映画「007は二度死ぬ」を題材にサンプルプロジェクトを作ってみます。

~/work$ mkdir you_only_live_twice && cd you_only_live_twice
~/work/you_only_live_twice$ bundle init
~/work/you_only_live_twice$ bundle exec rails new . -BJT
~/work/you_only_live_twice$ bundle exec rails g model Mi6::Agent name:string

こんな感じで、Mi6::Agent モデルを用意して(手順は大分端折ってます)、以下のようなクラス群をモデル層に定義してみます。

class Assassin
  def attack(agent)
    Rails.logger.info("Kill #{agent.name}!")
    agent.destroy
  end
end

class Villain
  def initialize
    @minion = Assassin.new
  end

  def encounter(agent)
    # 悪役は、エージェントに遭遇したら手下に殺させる
    @minion.attack(agent)
  end
end

class Mi6::Manager
  def infiltrate(agent, pretender, villain)
    # 協力者を使って、エージェントの死亡を偽装し、
    pretender.attack(agent)

    # 敵地に潜りこませようとするが、残念、お約束通り敵にバレる
    villain.encounter(agent)
  end
end

 

検証

require 'rails_helper'

RSpec.describe Mi6::Manager, type: :model do
  describe "#infiltrate" do
    let(:m)       { Mi6::Manager.new                       }
    let(:bond)    { create(:mi6_agent, name: "James Bond") }
    let(:ling)    { Assassin.new                           }
    let(:blofeld) { Villain.new                            }

    it do
      expect(bond).to receive(:destroy).twice

      m.infiltrate(bond, ling, blofeld)
    end
  end
end

こんなテストを書いて、実行してみると、

~/work/you_only_live_twice$ bundle exec rspec spec/models/mi6/manager_spec.rb
.

Finished in 0.0164 seconds (files took 1.17 seconds to load)
1 example, 0 failures

成功しました。確かに2回呼ばれているようです。
どんなSQLが発行されているのか、rails consoleから確認してみます。

irb(main):005:0> m.infiltrate(bond, ling, blofeld)
Kill James Bond!
   (0.2ms)  BEGIN
  SQL (0.3ms)  DELETE FROM `mi6_agents` WHERE `mi6_agents`.`id` = 13
   (2.9ms)  COMMIT
Kill James Bond!
   (0.1ms)  BEGIN
   (0.1ms)  COMMIT
=> #<Mi6::Agent id: 13, name: "James Bond", created_at: "2016-01-25 14:45:17", updated_at: "2016-01-25 14:45:17">

2回目のdestroy実行では、何も発行されていないことがわかります。
確かに、映画のボンドと同じようにARは2回死んではいませんでした。
 

最後に

さて。僕はこのサンプルを作るのに、映画のストーリーやら登場人物やらを調べて、えらい時間がかかりました。具体的な時間は恥ずかしいので内緒です。

マネーフォワードでは、疑問や課題に対して(僕のように)真摯に向き合えるエンジニアは絶賛募集中です。
一緒に厨ニっぽいクラス名とか設定とか考えたりして盛り上がりましょう!( @nyangry 談)

【採用サイト】
マネーフォワード採用サイト
Wantedly | マネーフォワード

【公開カレンダー】
マネーフォワード公開カレンダー

【プロダクト一覧】
家計簿アプリ・クラウド家計簿ソフト『マネーフォワード』
家計簿アプリ・クラウド家計簿ソフト『マネーフォワード』 iPhone,iPad
家計簿アプリ・クラウド家計簿ソフト『マネーフォワード』 Android
クラウド型会計ソフト『MFクラウド会計』
クラウド型請求書管理ソフト『MFクラウド請求書』
クラウド型給与計算ソフト『MFクラウド給与』
経費精算システム『MFクラウド経費』
消込ソフト・システム『MFクラウド消込』
マイナンバー対応『MFクラウドマイナンバー』
創業支援トータルサービス『MFクラウド創業支援サービス』
お金に関する正しい知識やお得な情報を発信するウェブメディア『マネトク!』

Pocket