こんにちは、分析基盤を担当しつつ Rails エンジニアのサポートもやっている鍵本です。
本日は Fast JSON API のバージョンアップをした時に突然テストが落ちて困った話をします。
背景
Zealsは、LINEおよびFacebook Messenger上で動作するチャットボットサービスで、WebのForm体験をチャット上で完結させることができます。この Zeals のアーキテクチャは同じくテックブログの記事『進化し続ける「Zeals」サービスを支えるアーキテクチャについて』でご紹介しているとおり、バックエンドを Ruby on Rails と React + Redux (& TypeScript) で実装しています。
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 オブジェクトでないことがわかっているから採用できる方法です。これで先のテストコードが成功することがわかります。
まとめ
今回は gem の一つである fast_jsonapi のアップデートで躓いた話についてご紹介しました。モデルの属性値として予約語を用いていなければ防げた問題だと思いますが、size という一般用語だったので仕方なかったかも知れません。とはいえ、このあたりは設計時に注意をしないといけない話ですね。
最後に
Zealsでは、発見した課題をワクワクしながら技術で解決するエンジニアを求めています!ご興味がある方は、ぜひ下記募集からご応募ください!