
こんにちは、分析基盤を担当しつつ Rails エンジニアのサポートもやっている鍵本です。
本日は Fast JSON API のバージョンアップをした時に突然テストが落ちて困った話をします。
github.com
- 背景
- テストが落ちる状態の再現
- エラーの原因
- 対応方法
- まとめ
- 最後に
背景
Zealsは、LINEおよびFacebook Messenger上で動作するチャットボットサービスで、WebのForm体験をチャット上で完結させることができます。この Zeals のアーキテクチャは同じくテックブログの記事『進化し続ける「Zeals」サービスを支えるアーキテクチャについて』でご紹介しているとおり、バックエンドを Ruby on Rails と React + Redux (& TypeScript) で実装しています。
tech.zeals.co.jp
Rails は DB から必要なデータを準備し JSON 形式に変換したものをフロント側の React に渡しています。その JSON 形式に変換する部分に Fast JSON API を用いています。アップデート前はバージョン 1.2 を用いておりテストコードも問題なく通っていたのですが、1.5 に gem のアップデートをしたところ突然テストが通らなくなりました。
テストが落ちる状態の再現
サンプルプログラムを使ってテストが落ちた状態を再現してみます。
属性として size, color を持つ Shirt モデルを作成します。
class Shirt < ApplicationRecord
enum size: { 'XS': 0, 'S': 1, 'M': 2, 'L': 3, 'XL': 4 }
end
Shirt モデルのためのシリアライザを以下のように作成します。
class ShirtSerializer < ActiveModel::Serializer
include FastJsonapi::ObjectSerializer
attributes :size, :color
end
Shirt モデルをシリアライズして JSON を返すコントローラを作成します。
class ShirtController < ApplicationController
def show
@shirt = Shirt.find(params[:id])
shirt_json = ShirtSerializer.new(@shirt).serializable_hash[:data][:attributes]
render json: shirt_json
end
end
ShirtController#show は {size: サイズ, color: 色} を返すので、例えば size: 'M' となる Shirt を生成して、その値が正しく得られることをテストするコードを用意します。
require 'rails_helper'
RSpec.describe ShirtController, type: :controller do
describe '#show' do
let(:shirt) { create(:shirt, size: 'M', color: 'black') }
subject { JSON.parse(response.body, symbolize_names: true) }
example 'response の size が M である' do
get :show, params: { id: shirt.id }
expect(subject[:size]).to eq shirt.size
end
end
end
fast_jsonapi のバージョンを 1.2 としてテストすると問題なく成功します。
$ bundle exec rspec spec/controllers/shirt_controller_spec.rb
.
Finished in 0.02659 seconds (files took 1.39 seconds to load)
1 example, 0 failures
ここで fast_jsonapi のバージョンを 1.5 にアップデートすると以下のような結果を得ます。
$ bundle exec rspec spec/controllers/shirt_controller_spec.rb
F
Failures:
1) ShirtController#show response の size が M である
Failure/Error: shirt_json = ShirtSerializer.new(@shirt).serializable_hash[:data][:attributes]
NoMethodError:
undefined method `each' for #<Shirt:0x00007f9478a3f318>
# ./app/controllers/shirt_controller.rb:5:in `show'
# ./spec/controllers/shirt_controller_spec.rb:10:in `block (3 levels) in <top (required)>'
Finished in 0.01942 seconds (files took 1.43 seconds to load)
1 example, 1 failure
Failed examples:
rspec ./spec/controllers/shirt_controller_spec.rb:9 # ShirtController#show response の size が M である
エラーの原因
fast_jsonapi の serializable_hash メソッドは ActiveRecord::Relation オブジェクト か否かで処理を分けているのですが、その判定方法が 1.2 以前とそれ以降で変わってしまったことが原因でした。具体的には 1.3 以降では以下のような条件で判定しています。
resource.respond_to?(:size) && !resource.respond_to?(:each_pair)
我々の Shirt モデルには enum で size という属性を持っているため size メソッドが存在します。そのため ActiveRecord::Relation オブジェクトであると間違って判定され、
@shirt.each do |s|
...
end
のような処理が実行され、each メソッドが存在しない というエラーが出たということでした。因みに 1.2 以前では以下のような判定だったので問題なかったというわけです。
resource.respond_to?(:each) && !resource.respond_to?(:each_pair)
対応方法
テーブルスキーマを変更して列名を size から別のものに変更すればいいのですが、影響範囲が大きいためシリアライズする際にオプションを渡すことで対応することとしました。すなわち、
class ShirtController < ApplicationController
def show
@shirt = Shirt.find(params[:id])
options = { is_collection: false }
shirt_json = ShirtSerializer.new(@shirt, options).serializable_hash[:data][:attributes]
render json: shirt_json
end
end
@shirt が ActiveRecord::Relation オブジェクトでないことがわかっているから採用できる方法です。これで先のテストコードが成功することがわかります。
続きを読む