Active Model Serializer で孫要素を含む場合のテストについて
はじめに
弊社では JSON を返却する API を開発する際に、Active Model Serializer を使用してオブジェクトをシリアライズしています。
普段の API の開発では先に Serializer とそのテストを実装し、その後に Controller とそのテストを実装することが多いです。
今回は例として以下のような孫要素(comments
)を含む JSON を返却する API を実装する場合について考えてみます。
{
"user": {
"name": "Alice",
"posts": [
{
"id": 1,
"title": "Hello",
"comments": [
{
"id": 1,
"content": "Nice to meet you!"
}
]
}
]
}
}
モデル構成は以下の通りです。
# Table name: users
#
# id :integer not null, primary key
# name :string not null
class User < ApplicationRecord
has_many :posts
end
# Table name: posts
#
# id :integer not null, primary key
# user_id :integer not null
# title :string not null
class Post < ApplicationRecord
belongs_to :user
has_many :comments
end
# Table name: comments
#
# id :integer not null, primary key
# post_id :integer not null
# content :string not null
class Comment < ApplicationRecord
belongs_to :post
end
Serializer の実装とテスト
Active Model Serializer ではモデルと同じように has_many
などを使って関連を定義することができます。
従って今回作成する Serializer は以下のようになります。
class UserSerializer < ActiveModel::Serializer
type :user
attributes :name
has_many :posts, serializer: PostSerializer
end
class PostSerializer < ActiveModel::Serializer
attributes :id, :title
has_many :comments, serializer: CommentSerializer
end
class CommentSerializer < ActiveModel::Serializer
attributes :id, :content
end
上記の Serializer のテストを書いてみます。
普段は Serializer 毎にテストを書いているので、まず CommentSerializer
のテストを書いてみます。
※ 期待する JSON となっているかを確認するために、 rspec-json_matcher
gem の be_json_as
というマッチャーを使用しています
RSpec.describe CommentSerializer, type: :serializer do
subject(:serializer) { described_class.new(comment) }
let(:comment) { create(:comment, content: 'content') }
it 'includes attributes as json format' do
expect(serializer.to_json).to be_json_as(
{
id: comment.id,
content: 'content'
}
)
end
end
次に PostSerializer
のテストを書いてみます。
ここで CommentSerializer
のテストは既に書いているので、comments
に関しては CommentSerializer
を使用して期待する JSON が返却されるかを確認します。
RSpec.describe PostSerializer, type: :serializer do
subject(:serializer) { described_class.new(post) }
let(:post) { create(:post, title: 'title') }
let!(:first_comment) { create(:comment, post:) }
let!(:second_comment) { create(:comment, post:) }
it 'includes attributes as json format' do
expect(serializer.to_json).to be_json_as(
{
id: post.id,
title: 'title',
comments: [
CommentSerializer.new(first_comment).as_json,
CommentSerializer.new(second_comment).as_json
]
}
)
end
end
最後に UserSerializer
のテストを書いてみます。
こちらも同様に PostSerializer
を使用して期待する JSON が返却されるかを確認します。
RSpec.describe UserSerializer, type: :serializer do
subject(:serializer) { described_class.new(user) }
let(:user) { create(:user, name: 'name') }
let(:first_post) { create(:post, user:) }
let(:second_post) { create(:post, user:) }
before do
create_list(:comment, 2, post: first_post)
create_list(:comment, 2, post: second_post)
end
it 'includes attributes as json format' do
expect(serializer.to_json).to be_json_as(
{
name: 'name',
posts: [
PostSerializer.new(first_post).as_json,
PostSerializer.new(second_post).as_json
]
}
)
end
end
しかしこのテストは失敗します。
失敗した原因は以下のように comments
が無いためです。
actual:
{
"name" => "name",
"posts" => [
[0] {
"id" => 7,
"title" => "title"
},
[1] {
"id" => 8,
"title" => "title"
}
]
}
reason: posts.[0].[
[0] "comments"
]
これはデフォルトではネストされたリレーションが表示されないためです。
公式のドキュメントを見てみると、 include
を指定することでネストされたリレーションを表示できることがわかります。
従って .to_json
で include
を指定するとテストが通るようになります。
it 'includes attributes as json format' do
# .to_json で include を指定する
expect(serializer.to_json(include: { posts: :comments })).to be_json_as(
{
name: 'name',
posts: [
PostSerializer.new(first_post).as_json,
PostSerializer.new(second_post).as_json
]
}
)
end
あとは Controller とそのテストを実装すれば完成です。
Controller の実装とテスト
主題は先ほどの Serializer だったので、Controller は簡単に実装します。
class UsersController < ApplicationController
def show
user = User.find(params[:id])
render status: :ok,
json: user,
serializer: UserSerializer,
include: { posts: :comments }, # ここでも include を指定する
adapter: :json
end
end
※ このままでは N+1 問題が発生するため includes
が必要ですが、今回は Serializer の include
と混同しないようにするため省略しています。
Controller のテスト(Request Spec)も書いてみます。
Serializer のテストは既に書いているので、期待する JSON を作成する際には Serializer を使用します。
RSpec.describe 'GET /users/:id' do
subject(:api_request) { get users_path(id) }
let(:user) { create(:user) }
let(:id) { user.id }
before do
create_list(:post, 2, user:)
create_list(:comment, 2, post: user.posts.first)
create_list(:comment, 2, post: user.posts.last)
end
it 'returns a response as json format' do
api_request
expect(response.body).to eq(
{
user: UserSerializer.new(user)
}.to_json(include: { posts: :comments }) # ここでも include を指定する
)
end
end
まとめ
今回は Active Model Serializer を使用して孫要素を含む JSON を返却する API の実装とテストについて紹介しました。
孫要素を含む場合は include
が必要になるため、実装やテストで注意が必要です。
一方で Active Model Serializer はしばらく開発が止まっているので、他の gem に移行することを検討しても良いかもしれません。
Discussion