LoginSignup
3
1

More than 5 years have passed since last update.

RSpec で複数の子要素を一斉更新するときに削除と追加が同時にできない

Last updated at Posted at 2017-02-06

やりたいこと

Rails で作った API で1対多のときに PATCH で JSON を投げると複数の子要素を同時に更新できるようにした。
が、しっかりとそのテストも書こうと思ったのだが思うように動かなかった。
具体的には

  • 削除だけ行う→可
  • 追加だけ行う→可
  • 削除と追加を同時に行う→不可

という状態だった。
原因は「 RSpec 側が渡したパラメータを勝手に改造しやがること 」だったので、それを直したい。

検証環境

  • User、Memo の2つのモデル
  • User には name のデータがある
  • Memo には content のデータがある
  • User は複数の Memo を持っている(1対多)

以下コード。

app/models/user.rb
class User < ApplicationRecord
  has_many :memos, dependent: :destroy
  accepts_nested_attributes_for :memos, allow_destroy: true
end
app/models/memo.rb
class Memo < ApplicationRecord
  belongs_to :user
end
app/controllers/users_controller.rb
class UsersController < ApplicationController
  def update
    @user = User.find(params[:id])

    respond_to do |format|
      if @user.update(user_params)
        format.json { render json: {message: 'ok'}, status: :created }
      else
        format.json { render json: @user.errors, status: :unprocessable_entitiy }
      end
    end
  end

  private
    def user_params
      params.require(:memo).permit(:name, memos_attributes: [:id, :user_id, :_destroy])
    end
end

app/models/user.rballow_destroy: true と書いているのと、
app/controllers/users_controller.rb のストロングパラメータで :_destroy を許可しているので、子要素の削除を行えるようにしている。

RSpec

describe 'PATCH /users/:id' do
  let!(:user) { create(:user) }
  let!(:memo) { create(:memo, user: user, content: 'content') }

  specify 'ユーザーが更新されること' do
    params = { user: {name: 'foobar', memos_attributes: [{id: memo.id, _destroy: 1}, {content: 'content2'}, {content: 'content3'}]} }
    patch user_url(user, format: :json), params: params

    ## 省略

    user.reload
    expect(user.memos.count).to eq (2)
  end
end

のように update メソッドに { user: {name: 'foobar', memos_attributes: [{id: memo.id, _destroy: 1}, {content: 'content2'}, {content: 'content3'}]} } というパラメータを投げるのだが、なぜか {id: memo.id, _destroy: 1} の部分が無視されて、最初に let! で作ったメモと合わせてメモの数が3つになりテストがコケた。

原因

ログを見ると、コードで記述したパラメータは上記にあるように

{ user: {name: 'foobar', memos_attributes: [{id: memo.id, _destroy: 1}, {content: 'content2'}, {content: 'content3'}]} }

だったのだが、実際に渡っているパラメータは

{ user: {name: 'foobar', memos_attributes: [{id: memo.id, _destroy: 1, content: 'content2'}, {content: 'content3'}]} }

のように
{id: memo.id, _destroy: 1}{content: 'content2'} がなぜか合体していたせいで、削除はされず、content が「content2」と「content3」の子要素が追加されるだけだった。

これは RSpec では、ハッシュの配列をフォームデータとしてデフォルトでは扱ってくれないことが原因だった。

解決策

こんだけ長ったらしく書いたものの
パラメータに

params = { user: {name: 'foobar', memos_attributes: [{id: memo.id, _destroy: 1}, {content: 'content2'}, {content: 'content3'}]} }.to_json

をつけ、PATCH で投げるときに

patch user_url(user, format: :json), params: params, headers { "CONTENT_TYPE": "application/json", "ACCEPT": "application/json" }

のように投げるとハッシュが合体せずにパラメータが渡る。

まとめ

feature スペックとかで書けばそもそも PATCH で投げる必要もないが、
今回のように API の update をテストする場合かつ子要素の更新もする時はこんな感じできる。
ちなみに私が実際に更新したかった子要素は base64 でエンコードされた画像であったためログがカオスになった。(さっさとファイル名をテキトーな文字列に置き換えろ)

まあ何にせよログって大事だね

参考・関連

stack overflow

3
1
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
1