やりたいこと
Rails で作った API で1対多のときに PATCH で JSON を投げると複数の子要素を同時に更新できるようにした。
が、しっかりとそのテストも書こうと思ったのだが思うように動かなかった。
具体的には
- 削除だけ行う→可
- 追加だけ行う→可
- 削除と追加を同時に行う→不可
という状態だった。
原因は「 RSpec 側が渡したパラメータを勝手に改造しやがること 」だったので、それを直したい。
検証環境
- User、Memo の2つのモデル
- User には
name
のデータがある - Memo には
content
のデータがある - User は複数の Memo を持っている(1対多)
以下コード。
class User < ApplicationRecord
has_many :memos, dependent: :destroy
accepts_nested_attributes_for :memos, allow_destroy: true
end
class Memo < ApplicationRecord
belongs_to :user
end
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.rb
で allow_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 でエンコードされた画像であったためログがカオスになった。(さっさとファイル名をテキトーな文字列に置き換えろ)
まあ何にせよログって大事だね