MOE3: SupportOrder のテスト

Order#target と Order#target=

理想は TDD なんだけど、実際に処理を書いてみないと挙動のイメージがつかめないことが多いので、どうしてもある程度コードが形になってからテストを用意するスタイルになってしまう。

安定してきたらテスト先行に移れると思うけど当面はやむなしか。

実装は Order だけど、実際に使用されるのは SupportOrderConvoyOrder なので、SupportOrder のテストとして書いた。ConvoyOrder のテストは書くとしても差分が必要な場合だけになる予定。

spec/models/support_order_spec.rb

初めて shared_contextlet を活用してみたので、いろいろ実験した結果長くなった。試しに全文貼ってみる。

正直 SupportOrder#new で生成した場合の挙動や target 属性未指定で生成した場合の挙動はテストする必要があったのか疑問だけど、「どうなるか分からない」で放置するのも怖かったので書くだけ書いておいた。

行軍解決処理のテストはこの何倍も長くなるんだろうなあ。

# -*- coding: utf-8 -*-
require 'spec_helper'

describe SupportOrder do
  let(:hold_order) { FactoryGirl.create :hold_order }
  let(:move_order) { FactoryGirl.create :move_order }

  shared_context "SupportOrder#new で生成", support_order_gen: :new do
    let(:support_order) { FactoryGirl.build :support_order, target: target }
  end

  shared_context "SupportOrder#create で生成", support_order_gen: :create do
    let(:support_order) { FactoryGirl.create :support_order, target: target }
  end

  shared_context "target が nil の場合", target: :nil do
    let(:target) { nil }
  end

  shared_context "target が HoldOrder の場合", target: :hold do
    let(:target) { hold_order }
  end

  shared_context "target が MoveOrder の場合", target: :move do
    let(:target) { move_order }
  end

  describe "#new", support_order_gen: :new do
    context "target が nil の場合", target: :nil do
      subject { support_order.target }
      example { expect(subject).to be_nil }
    end

    context "target が HoldOrder の場合", target: :hold do
      subject { support_order.target }
      example { expect(subject).not_to be_nil }
    end

    context "target が MoveOrder の場合", target: :move do
      subject { support_order.target }
      example { expect(subject).not_to be_nil }
    end
  end

  describe "#create", support_order_gen: :create do
    context "target が nil の場合", target: :nil do
      subject { support_order.target }
      example { expect(subject).to be_nil }
    end

    context "target が HoldOrder の場合", target: :hold do
      subject { support_order.target }
      example { expect(subject).not_to be_nil }
    end

    context "target が MoveOrder の場合", target: :move do
      subject { support_order.target }
      example { expect(subject).not_to be_nil }
    end
  end

  describe "#target", support_order_gen: :new do
    context "target が HoldOrder の場合", target: :hold do
      subject { support_order.target }
      example { expect(subject).not_to eq hold_order }
      example { expect(subject.id).to be_nil }
      example { expect(subject.sample).to be_true }
      example { expect(subject.unit).to eq hold_order.unit }
      example { expect(subject.province).to eq hold_order.province }
      example { expect(subject.persisted?).to be_false }
    end

    context "target が MoveOrder の場合", target: :move do
      subject { support_order.target }
      example { expect(subject).not_to eq move_order }
      example { expect(subject.id).to be_nil }
      example { expect(subject.sample).to be_true }
      example { expect(subject.unit).to eq move_order.unit }
      example { expect(subject.src).to eq move_order.src }
      example { expect(subject.dst).to eq move_order.dst }
      example { expect(subject.persisted?).to be_false }
    end
  end

  describe "#target", support_order_gen: :create do
    context "target が HoldOrder の場合", target: :hold do
      subject { support_order.target }
      example { expect(subject).not_to eq hold_order }
      example { expect(subject.id).not_to be_nil }
      example { expect(subject.sample).to be_true }
      example { expect(subject.unit).to eq hold_order.unit }
      example { expect(subject.province).to eq hold_order.province }
      example { expect(subject.persisted?).to be_true }
    end

    context "target が MoveOrder の場合", target: :move do
      subject { support_order.target }
      example { expect(subject).not_to eq move_order }
      example { expect(subject.id).not_to be_nil }
      example { expect(subject.sample).to be_true }
      example { expect(subject.unit).to eq move_order.unit }
      example { expect(subject.src).to eq move_order.src }
      example { expect(subject.dst).to eq move_order.dst }
      example { expect(subject.persisted?).to be_true }
    end
  end

  describe "#target=", support_order_gen: :new do
    context "後から設定", target: :nil do
      before do
        support_order.target = hold_order
      end

      subject { support_order.target }
      example { expect(subject).not_to be_nil }
      example { expect(subject).not_to eq hold_order }
      example { expect(subject.unit).to eq hold_order.unit }
      example { expect(subject.province).to eq hold_order.province }
      example { expect(subject.persisted?).to be_false }
    end

    context "書き換え", target: :hold do
      before do
        @lt = support_order.target
        support_order.target = move_order
      end

      subject { support_order.target }
      example { expect(subject).not_to be_nil }
      example { expect(subject).not_to eq move_order }
      example { expect(subject.unit).to eq move_order.unit }
      example { expect(subject.src).to eq move_order.src }
      example { expect(subject.dst).to eq move_order.dst }
      example { expect(subject.persisted?).to be_false }
      example { expect(@lt.persisted?).to be_false }
    end
  end

  describe "#target=", support_order_gen: :create do
    context "後から設定", target: :nil do
      before do
        support_order.target = hold_order
      end

      subject { support_order.target }
      example { expect(subject).not_to be_nil }
      example { expect(subject).not_to eq hold_order }
      example { expect(subject.unit).to eq hold_order.unit }
      example { expect(subject.province).to eq hold_order.province }
      example { expect(subject.persisted?).to be_true }
    end

    context "書き換え", target: :hold do
      before do
        @lt = support_order.target
        support_order.target = move_order
      end

      subject { support_order.target }
      example { expect(subject).not_to be_nil }
      example { expect(subject).not_to eq move_order }
      example { expect(subject.unit).to eq move_order.unit }
      example { expect(subject.src).to eq move_order.src }
      example { expect(subject.dst).to eq move_order.dst }
      example { expect(subject.persisted?).to be_true }
      example { expect(@lt.persisted?).to be_false }
    end
  end
end

Order#target= リファクタリング

少しだけ綺麗になった。

class Order < ActiveRecord::Base
  #(略)

  # target に渡された Order は自動的に複製される
  alias_method :association_target=, :target=
  def target=(target)
    if target
      sample = target.dup
      sample.sample = true
    else
      sample = nil
    end
    self.target.destroy if self.target
    self.association_target = sample

    save! if persisted?
  end

  #(略)
end

belongs_to 宣言によって設定された Order#target= メソッドは Order.create(および Order.new)の実行時にも呼ばれる。

引数として nilOrder の派生クラスのオブジェクト以外が渡された場合の動きは考慮していないが、その場合は sample.sample = trueNoMethodError 例外が飛ぶ。

万が一 sample= メソッドを持つオブジェクトでも self.assosiation_target = sampleActiveRecord::AssociationTypeMismatch 例外になるので検出漏れはない。

例外が飛ぶ時点で呼び出し側のバグは明白なので、Order#target= としてはこれ以上の配慮は不要だろう。