🐷

Active Model Serializer で孫要素を含む場合のテストについて

2024/11/19に公開

はじめに

弊社では JSON を返却する API を開発する際に、Active Model Serializer を使用してオブジェクトをシリアライズしています。
https://github.com/rails-api/active_model_serializers

普段の 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 というマッチャーを使用しています
https://github.com/r7kamura/rspec-json_matcher

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 を指定することでネストされたリレーションを表示できることがわかります。
https://github.com/rails-api/active_model_serializers/blob/0-10-stable/docs/general/adapters.md#include-option

従って .to_jsoninclude を指定するとテストが通るようになります。

  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 に移行することを検討しても良いかもしれません。

GitHubで編集を提案
SocialPLUS Tech Blog

Discussion