LoginSignup
6

More than 3 years have passed since last update.

Google Books APIから情報を格納するモデルを作り、直感的な扱いとテストを可能にする

Last updated at Posted at 2020-05-23

対象読者

  • 本に関するRailsポートフォリオを作る方
  • Google Books APIを用いたRailsポートフォリオを作る方

本記事の到達物

Google Books APIのIDから情報を取得し、クラスに格納する

pry(main)> @google_book = GoogleBook.new_from_id('c1L4IzmUzicC')

pry(main)> @google_book.title
=> "Practical Rails Plugins"

pry(main)> @google_book.authors
=> ["Nick Plante", "David Berube"]

pry(main)> @google_book.image
=> "http://books.google.com/books/content?id=c1L4IzmUzicC&printsec=frontcover&img=1&zoom=5&edge=curl&imgtk=AFLRE73ENsMYFOfY27vluLqgI1cO-b80lA7enoeZzzcDGEhA5NWIj3djHvd6gvP1zlKoMoC4V0_7fKVuIjWQDYVs4FrDjHvxoqtRUcxHZ9L7isRtsHc2Cs5iS6DPAQQcTT20Oseo9gq_&source=gbs_api"

キーワードから検索し、複数のオブジェクトを返す

pry(main)> @google_books = GoogleBook.search('Rails')

pry(main)> @google_books[0].title
=> "実践Rails"

pry(main)> @google_books.last.authors
=> ["Sam Ruby", "Dave Thomas", "David Heinemeier Hansson"]

pry(main)> @google_books.map { |google_book| google_book.title }
=> ["実践Rails",
 "独習Ruby on Rails",
 "Railsレシピ",
 "Ajax on Rails",
 "Ruby on Rails 4アプリケーションプログラミング",
 "Ruby on Rails 5の上手な使い方 現場のエンジニアが教えるRailsアプリケーション開発の実践手法",
 "Ruby on Rails 5 超入門",
 "RailsによるアジャイルWebアプリケーション開発第3版",
 "JRuby on Rails 実践開発ガイド",
 "RailsによるアジャイルWebアプリケーション開発第4版"]

pry(main)> @google_books.class
=> Array

格納された情報を、複数のテーブルに保存できる


pry(main)> @google_book = GoogleBook.new_from_id('wlNHDwAAQBAJ')

pry(main)> @google_book.title                                   
=> "Ruby on Rails 5の上手な使い方 現場のエンジニアが教えるRailsアプリケーション開発の実践手法"

pry(main)> @google_book.authors
=> ["太田 智彬", "寺下 翔太", "手塚 亮", "宗像 亜由美", "株式会社リクルートテクノロジーズ"]

pry(main)> @google_book.save
=> true


pry(main)> @book = Book.last

pry(main)> @book.title
=> "Ruby on Rails 5の上手な使い方 現場のエンジニアが教えるRailsアプリケーション開発の実践手法"

pry(main)> @book.authors[0].name
=> "太田 智彬"

pry(main)> @book.authors.size
=> 5

pry(main)> @book.authors.class
=> Author::ActiveRecord_Associations_CollectionProxy


pry(main)> @author = Author.last

pry(main)> @author.name
=> "株式会社リクルートテクノロジーズ"

 本記事で書くこと

Google Books APIからの情報を格納する、GoogleBookモデルを作成します。
大きなメリットとしては、以下の2つがあります。

  • Google Books APIから受け取った情報をControllerやViewで直感的に扱える。
  • 情報を取得する / 情報を格納して整理する / 複数テーブルに跨いで保存をする、などのロジックを分離して書くことができ、それぞれテストを行える。

本記事ではControllerでの使用例、テストについても記載します。

DB設計

データベースには、本の以下の情報を保存します。

  • 「タイトル」
  • 「著者」
  • 「画像URL」
  • 「出版日」
  • 「Google Books APIのID」

もちろん、Google Books APIにある情報であれば、これ以外も取得し保存することができます。
本記事では説明のために、取得する情報としては少なめにしてあります。

ER図

名称未設定ファイル.png

BooksテーブルとAuthorsテーブルを用意します。
一つの本に対し、著者は複数人いることがあるので、book has_many authorsの関係にします。

ただし、「本を一覧で表示する」ようなページで、著者全員でなく代表著者を表示したい場合があると思います。
そのため、authorsテーブルにはis_representativeカラムを用意しておきます。

また、「Google Books APIから情報を取得するなら、自前のデータベースに情報を持つ必要は無いのでは?」と思われるかもしれません。
その設計を行い、失敗した話を以下に載せておきます。
GoogleBooksAPIだけで本リソースの取得をする設計を行い、失敗した話

要約すると、本の情報は自前のデータベースでも情報を持っておいた方が良い、という結論になります。

マイグレーションファイル

マイグレーションファイルを作るなら、以下のようになるかと思います。

 Booksテーブル
db/migrate/20202020202020_create_books
class CreateBooks < ActiveRecord::Migration[5.2]
  def change
    create_table :books do |t|
      t.string :google_books_api_id, null: false
      t.string :title, null: false
      t.string :image
      t.date :published_at

      t.timestamps
    end

    add_index :books, :googlebooksapi_id, unique: true
  end
end

Google Books APIのIDは必ず必要なため、null: falseを指定しておきます。
また、Google Books APIのIDが重複することは考えられないため、ユニークキーも付与します。

タイトルが存在しない本は、存在しないと考えられるため、null: falseを指定しておきます。

逆に、他の情報にnull: falseを指定するのには注意が必要です。
外部APIからの情報なわけで、本によってはその情報が無いことがあり、「DBに登録できない」という事態を発生させうるからです。

 Authorsテーブル
db/migrate/20202020202021_create_authors
class CreateAuthors < ActiveRecord::Migration[5.2]
  def change
    create_table :authors do |t|
      t.references :book, foreign_key: true
      t.string :name, null: false
      t.boolean :is_representative, null: false

      t.timestamps
    end
  end
end

Google Books APIを叩くメソッド

まずはAPIを叩くモジュールをapp/lib/配下に追加します。

app/lib/google_books_api.rb
module GoogleBooksApi
  def url_of_creating_from_id(googlebooksapi_id)
    "https://www.googleapis.com/books/v1/volumes/#{googlebooksapi_id}"
  end
  #  Google Books APIのIDから、APIのURLを取得する

  def url_of_searching_from_keyword(keyword)
    "https://www.googleapis.com/books/v1/volumes?q=#{keyword}&country=JP"
  end
  #  キーワードから、検索するAPIのURLを取得する

  def get_json_from_url(url)
    JSON.parse(Net::HTTP.get(URI.parse(Addressable::URI.encode(url))))
  end
  #  URLから、JSON文字列を取得し、JSONオブジェクトを構築する
end

本記事で使用するGoogle Books APIは2種類です。

  • IDから一つの本の情報を返してくれる

https://www.googleapis.com/books/v1/volumes/:ID
というURLによって取得できます。
以下のURLがその例です。
https://www.googleapis.com/books/v1/volumes/aB4B13xGEv4C

Google Books APIから取得できる情報がどんなものか知らない場合、上のURLを見て確認してみて下さい。
タイトル、出版日、購入リンク、ISBN、など色々取得できることが分かると思います。

  • キーワードから検索結果群を返してくれる

https://www.googleapis.com/books/v1/volumes?q=search?:キーワード
というURLによって取得できます。
以下のURLがその例です。
https://www.googleapis.com/books/v1/volumes?q=search?Rails

Google Books APIの他仕様について知りたい方は、公式ドキュメントをご参照ください。
Getting Started

URLをエスケープするために、addressableというgemを使っています。
以下gemをGemfileに追記しbundle installしてください。

Gemfile
gem 'addressable'

ちなみに、Railsでは、app/**/**.rbを自動で読み込んでくれます。
よって使いたいクラス内でinclude GoogleBooksApiincludeさえすれば、以上3つのメソッドをクラス内で使用できます。

情報を格納するモデル

Google Books APIからの情報を、オブジェクトとして格納するモデルを以下のように作ります。

app/models/google_book.rb
class GoogleBook
  include ActiveModel::Model
  include ActiveModel::Attributes
  include ActiveModel::Validations

  attribute :googlebooksapi_id, :string
  attribute :authors
  attribute :image, :string
  attribute :published_at, :date
  attribute :title, :string

  validates :googlebooksapi_id, presence: true
  validates :title, presence: true

  class << self
    include GoogleBooksApi

    def new_from_item(item)
      @item = item
      @volume_info = @item['volumeInfo']
      new(
        googlebooksapi_id: @item['id'],
        authors: @volume_info['authors'],
        image: image_url,
        published_at: @volume_info['publishedDate'],
        title: @volume_info['title'],
      )
    end

    def new_from_id(googlebooksapi_id)
      url = url_of_creating_from_id(googlebooksapi_id)
      item = get_json_from_url(url)
      new_from_item(item)
    end

    def search(keyword)
      url = url_of_searching_from_keyword(keyword)
      json = get_json_from_url(url)
      items = json['items']
      return [] unless items

      items.map do |item|
        GoogleBook.new_from_item(item)
      end
    end

    private

    def image_url
      @volume_info['imageLinks']['smallThumbnail'] if @volume_info['imageLinks'].present?
    end
  end

  def save
    return false unless valid?

    book = build_book
    return false unless book.valid?

    ActiveRecord::Base.transaction do
      book.remote_image_url = image if image.present?
      book.save
      authors.each.with_index do |author, index|
        author = book.authors.build(name: author)
        author.is_representation = index.zero?
        author.save
      end
    end
    true
  end

  def find_book_or_save
    if Book.find_by(googlebooksapi_id: googlebooksapi_id) || save
      Book.find_by(googlebooksapi_id: googlebooksapi_id)
    else
      false
    end
  end

  private

  def build_book
    Book.new(
      googlebooksapi_id: googlebooksapi_id,
      published_at: published_at,
      title: title,
    )
  end
end

長くなりました笑
一つ一つ解説していきます。

ActiveModel

  include ActiveModel::Model
  include ActiveModel::Attributes
  include ActiveModel::Validations

ActiveModelとは「データベースと連携しないActiveRecord」みたいなものです。

Active Model の基礎 - Railsガイド

ActiveModelのattribute

  attribute :googlebooksapi_id, :string
  attribute :authors
  (以下略)

ActiveModel::Attributesincludeしたので使えます。
authorsには配列を入れることを想定していますが、配列に対応するattributeの型は無いようなので、この書き方にしています。

ActiveModel::Attributes が最高すぎるんだよな。

ActiveModelのvalidates

  validates :googlebooksapi_id, presence: true
  validates :title, presence: true

Google Books APIのIDが無い本、タイトルの存在しない本は存在しない(と思われる)ので、validetesを入れておきます。
仮にGoogle Books APIのIDが無い本がオブジェクトとして格納された場合、valid?メソッドを使ったときにfalseを返すことができます。(後述しますが、saveメソッドにて使用します)

クラスメソッドの定義方法

  class << self

new_from_idGoogleBook.new_from_id('c1L4IzmUzicC')のようにクラスメソッドとして使いたいメソッドです。クラスメソッドを定義する方法は他にもありますが、class << slefによるやり方が推奨なようです。

Rubyのクラスメソッドをclass << selfで定義している理由(翻訳)

IDからインスタンスを生成する

    def new_from_id(googlebooksapi_id)
      url = url_of_creating_from_id(googlebooksapi_id)
      item = get_json_from_url(url)
      new_from_item(item)
    end

順番がちょっと前後しますが、new_from_itemより先にnew_from_idを説明します。

前述のGoogleBooksApiモジュールのurl_of_creating_from_idおよびget_json_from_urlを使用することで、一つの本の情報(JSON)をitemとして取得します。
そのitemをnew_from_itemに渡します。

itemからインスタンスを生成する

    def new_from_item(item)
      @item = item
      @volume_info = @item['volumeInfo']
      new(
        googlebooksapi_id: @item['id'],
        authors: @volume_info['authors'],
        image: image_url,
        published_at: @volume_info['publishedDate'],
        title: @volume_info['title'],
      )
    end

Google Books APIの中身を観察すると分かりますが、多くの情報はitem['volumeInfo']に入っています。
取り出したい情報を上記のように適宜定義し、new()を使ってインスタンスを生成する、というロジックにします。
image_urlだけ下記のやり方で実装しています。

image_urlの実装

    private

    def image_url
      @volume_info['imageLinks']['smallThumbnail'] if @volume_info['imageLinks'].present?
    end

本によってはvolume_info['imageLinks']が入っていない本が存在するため、そのままvolume_info['imageLinks']['smallThumbnail']だけで使おうとするとundefindのエラーが出てしまうことがあります。
undefindを出さないようにするため、上記のように実装します。

また、このimage_urlメソッドはクラス外で使用することは考えられないので、private下で定義します。

検索結果をインスタンス群として返すクラスメソッド

    def search(keyword)
      url = url_of_searching_from_keyword(keyword)
      json = get_json_from_url(url)
      items = json['items']
      return [] unless items

      items.map do |item|
        GoogleBook.new_from_item(item)
      end
    end

Google Books APIのうち、検索結果群を返してくれるAPIを使った場合、返ってくるのは複数のitemsとなります。
itemsは配列として[item1, item2, item3, ...]という形になっています。
mapメソッド内で一つ一つnew_from_item(item)をすることで、[googlebook1, googlebook2, googlebook3, ...]という配列の形で返すことができます。

不適切なキーワードだと検索結果群を返してくれるAPIでitemsが返ってこないため、その場合もundefindのエラーが発生してしまいます。
そこでreturn [] unless itemsの一行を入れることで、itemsが無い場合は空の配列を返すようにします。

まだsaveメソッド、find_book_or_saveメソッドの説明が残っていますが、先にControllerでの使い方を見るほうが分かりやすいと思うので、Controllerの説明に移りたいと思います。

Controllerでの使用例

GoogleBookクラスを使うのは、「検索画面」と「リソース登録(create)」の2つかと思います。

ルーティング

config/routes.rb
  resources :books, only: %i[create show], shallow: true do
    collection do
      get :search
    end
  end

あらかじめ、上記のようなルーティングは設定しておきます。
本筋とズレてしまうので説明は省きます。

検索画面のController

app/controllers/books_controller.rb
class BooksController < ApplicationController
  def search
    @search_form = SearchBooksForm.new(search_books_params)
    books = GoogleBook.search(@search_form.keyword)
    @books = Kaminari.paginate_array(books).page(params[:page])
  end

  private

  def search_books_params
    params.fetch(:q, keyword: '').permit(:keyword)
  end
end

弊記事で恐縮ですが、検索フォームは以下の記事のやり方で実装します。
【Ruby on Rails】フォームオブジェクトを使って検索キーワードをcontrollerに送る

受け取ることのできた検索キーワード(@search_form.keyword)をGoogleBook.searchに渡して、検索結果をGoogleBookクラスから生成したインスタンス群の配列として受け取ります。

以降は任意ですが、私は検索結果をkaminariのページネーションで表示したかったため、Kaminari.paginate_arrayに渡して使用しました。

リソース登録のController ver.1(今までのメソッドのみで書くNGな例)

やりたいこととしては以下の通りです。

  • GoogleBookモデルに持っていた情報をBookモデルやAuthorモデルに格納し直す
  • ヴァリデーションをかける
  • ヴァリデーション成功時にはDBに保存する
app/models/google_book.rb
class BooksController < ApplicationController
  def create
    google_book = GoogleBook.new_from_id(create_book_params[:googlebooksapi_id])
    @book = Book.build(
              googlebooksapi_id: google_book.googlebooksapi_id,
              published_at: google_book.published_at,
              title: google_book.title,
            )
    if @book.valid?
      @book.remote_image_url = google_book.image if google_book.image.present?
      @book.save
      google_book.authors.each.with_index do |author, index|
        @author = @book.authors.build(name: author)
        @author.is_representation = index.zero?
        @author.save
      end
      redirect_to @book
    else
      redirect_to search_books_path, danger: 'ページの表示に失敗しました'
    end
  end

  private

  def create_book_params
    params.permit(:googlebooksapi_id)
  end
end

しかし、上記の実装には色々な問題が発生しています。

  • Fat Controllerになってしまっている
  • 重複した本を弾けていない
  • 複数リソースへのデータ登録なのに、ActiveRecord::Base.transactionを使えていない

特にFat Controllerの問題はなんとかしなくてはいけないです。
そもそも、このDBへの保存に関する処理はGoogleBookモデルが責務を持つ処理かと思います。
よって、保存に関するロジックはGoogleBookのインスタンスメソッドとして定義することにします。

複数テーブルへの保存を実現する、saveメソッド

app/models/google_book.rb
  def save
    return false unless valid?
    book = build_book

    ActiveRecord::Base.transaction do
      book.remote_image_url = image if image.present?
      book.save
      if authors.present?
        authors.each.with_index do |author, index|
          author = book.authors.build(name: author)
          author.is_representative = index.zero?
          author.save
        end
      end
    end
    true
  end

  private

  def build_book
    Book.new(
      googlebooksapi_id: googlebooksapi_id,
      published_at: published_at,
      title: title,
    )
  end

ActiveRecordsaveメソッド風に仕上げるため、保存に成功すればtrue、失敗すればfalseを返すようにしたいです。
そこで、return false unless valid?の一行で、valid?で失敗したときにはfalseを返すようにしました。
また、成功時の最後にtrueの一行を入れることで、成功時にはtrueを返すことができます。

ActiveRecord::Base.transactionで囲うことで、複数リソース登録時で途中で失敗したときにロールバックを走らせることができます。
この場合では、何かの不具合で後半のauthor.saveで失敗したとしても、前半のbook.saveを取り止めにすることができます。

book.remote_image_url = image if image.present?はCarrierwaveで画像のアップロードを行うロジックになります。本筋とズレるので、今回は説明は省きます。

author.is_representative = index.zero?は、authors配列のうち、最初のインデックスにある著者を「代表著者」とするための一行です。
each.with_indexを使って配列を回してるのもこれが理由です。

リソース登録のController ver.2(重複の登録をしてしまう)

app/controllers/books_controller.rb
class BooksController < ApplicationController
  def create
    google_book = GoogleBook.new_from_id(create_book_params[:googlebooksapi_id])
    if google_book.save
      @book = Book.find_by(googlebooksapi_id: google_book.googlebooksapi_id)
      redirect_to @book
    else
      redirect_to search_books_path, danger: 'ページの表示に失敗しました'
    end
  end

  private

  def create_book_params
    params.permit(:googlebooksapi_id)
  end
end

かなりスッキリしましたが、まだ問題が残っています。
Google Books APIのIDが重複した、すなわち同じ本を登録してしまう可能性があります。

find_or_create_by風味な実装

実現したいのは以下のことです。

  • すでにbooksテーブルにその本があるなら、そのレコードに対応したモデルを返す
  • なければ、saveメソッドを実行して、新しく作ったレコードに対応したモデルを返す
  • booksテーブルにその本が無く、saveメソッドが失敗するなら、falseを返す

ActiveRecordで言えば、find_or_create_byに近い挙動を実現したいことになります。
GoogleBookのインスタンスメソッドとして、以下のようにfind_book_or_saveメソッドとして実装します。

app/models/google_book.rb
  def find_book_or_save
    if Book.find_by(googlebooksapi_id: googlebooksapi_id) || save
      Book.find_by(googlebooksapi_id: googlebooksapi_id)
    else
      false
    end
  end

リソース登録のController ver.3(完成)

app/controllers/books_controller.rb
class BooksController < ApplicationController
  def create
    google_book = GoogleBook.new_from_id(create_book_params[:googlebooksapi_id])
    if (@book = google_book.find_book_or_save)
      redirect_to @book
    else
      redirect_to search_books_path, danger: 'ページの表示に失敗しました'
    end
  end

  private

  def create_book_params
    params.permit(:googlebooksapi_id)
  end
end

RSpecでテストを書く

今回扱うテストは3種類とします。

  • Google Books APIを叩くメソッドのテスト
  • GoogleBookのモデルテスト
  • 本の登録時のリクエストスペック

テストの書き方については若干自信が無いため、ご指摘お待ちしております笑

Google Books APIを叩くメソッドのテスト

spec/lib/google_books_api_spec.rb
require 'rails_helper'

describe GoogleBooksApi do
  let(:test_class) { Struct.new(:google_books_api) { include GoogleBooksApi } }
  let(:google_books_api) { test_class.new }

  it '検索するAPIを叩き、複数のデータを返すkindが取得できること' do
    url = google_books_api.url_of_searching_from_keyword('Rails')
    expect(google_books_api.get_json_from_url(url)['kind']).to eq 'books#volumes'
  end

  it 'IDから本の情報を取得するAPIを叩き、特定データを返すkindが取得できること' do
    GOOGLE_BOOKS_API_ID_SAMPLE = 'aB4B13xGEv4C'.freeze
    url = google_books_api.url_of_creating_from_id(GOOGLE_BOOKS_API_ID_SAMPLE)
    expect(google_books_api.get_json_from_url(url)['kind']).to eq 'books#volume'
    expect(google_books_api.get_json_from_url(url)['id']).to eq GOOGLE_BOOKS_API_ID_SAMPLE
  end
end

モジュールのテストをするやり方に関しては以下サイトを参考にさせて頂きました。

【Ruby on Rails】初心者でもメソッドの単体テストがしたい!!

GoogleBookのモデルテスト

まずはFactoryBotを定義しておきます。

spec/factories/google_book.rb
FactoryBot.define do
  factory :google_book do
    googlebooksapi_id { 'wlNHDwAAQBAJ' }
    authors do
      [
        '太田 智彬',
        '寺下 翔太',
        '手塚 亮',
        '宗像 亜由美',
        '株式会社リクルートテクノロジーズ'
      ]
    end
    image { 'http://books.google.com/books/content?id=wlNHDwAAQBAJ&printsec=frontcover&img=1&zoom=5&edge=curl&imgtk=AFLRE70j5lrdzOYN-iUu8w-G_JJKpEhnpUGAgqyZd7rj4jHu59NcAU48eQ75T4fkdyyZD6dMlwjjw0sAdQSKY_HiEdNBMMeyDn4DUmOcY-oLHFRAnxPXocc_T_PA7NYdSlZdwKckhCMy&source=gbs_api' }
    published_at { '2018-01-24' }
    title { 'Ruby on Rails 5の上手な使い方 現場のエンジニアが教えるRailsアプリケーション開発の実践手法' }
  end
end

現場Railsです。
次にモデルテストの本体です。

app/models/google_book_spec.rb
require 'rails_helper'

RSpec.describe GoogleBook, type: :model do
  it '有効なファクトリを持つこと' do
    google_book = build(:google_book)
    expect(google_book).to be_valid
  end

  it 'Google Books APIのIDが存在しないときに無効なこと' do
    google_book = build(:google_book, googlebooksapi_id: nil)
    google_book.valid?
    expect(google_book.errors.messages[:googlebooksapi_id]).to include('を入力してください')
  end

  it 'タイトルが存在しないときに無効なこと' do
    google_book = build(:google_book, title: nil)
    google_book.valid?
    expect(google_book.errors.messages[:title]).to include('を入力してください')
  end

  it 'Google Books APIのIDから目的のインスタンスを生成できること' do
    googlebooksapi_id = 'YEfUBgAAQBAJ'
    google_book = GoogleBook.new_from_id(googlebooksapi_id)
    expect(google_book.title).to eq 'SpriteKitではじめる2Dゲームプログラミング Swift対応'
    expect(google_book.googlebooksapi_id).to eq googlebooksapi_id
    expect(google_book.authors).to eq %w[山下佳隆 村田知常 原知愛 近藤秀彦]
    expect(google_book.author).to eq '山下佳隆'
  end

  it '適切なキーワードから複数の検索結果を返し、そのタイトルにキーワードが含まれていること' do
    keyword = 'Ruby'
    keyword_count = 0
    google_books = GoogleBook.search(keyword)
    expect(google_books.size).to be >= 5 #  検索結果を5個以上は返せる
    google_books.each do |google_book|
      if google_book.title.include?(keyword)
        keyword_count += 1
      end
    end
    expect(keyword_count).to be >= 5 #  キーワードのRubyを含むタイトルが5個以上は返せる
  end

  it '不適切なキーワードからは検索結果を返さないこと' do
    keyword = 'bbvjnaovnaov' #  適当
    google_books = GoogleBook.search(keyword)
    expect(google_books.size).to be 0
  end

  describe '保存時に' do
    context '不適切な情報しか持たないときは' do
      let(:google_book) { build(:google_book, googlebooksapi_id: nil) }
      it '保存に失敗すること' do
        expect { google_book.save }.to change { Book.count }.by(0).and change { Author.count }.by(0)
      end
      it 'falseを返すこと' do
        expect(google_book.save).not_to be_truthy
      end
    end

    context '適切な情報を持っているときは' do
      let(:google_book) { build(:google_book, authors: [
                                  '太田 智彬',
                                  '寺下 翔太',
                                  '手塚 亮',
                                  '宗像 亜由美',
                                  '株式会社リクルートテクノロジーズ'
                                ])
      }
      it '保存できること' do
        expect { google_book.save }.to change { Book.count }.by(1).and change { Author.count }.by(5)
      end
      it 'trueを返すこと' do
        expect(google_book.save).to be_truthy
      end
    end

    context '著者の情報だけを持っていないときにも' do
      let(:google_book) { build(:google_book, authors: nil) }
      it '保存できること' do
        expect { google_book.save }.to change { Book.count }.by(1).and change { Author.count }.by(0)
      end
      it 'trueを返すこと' do
        expect(google_book.save).to be_truthy
      end
    end
  end
end

ヴァリデーションと、それぞれのメソッドをテストしただけなので、あまり説明は要らないかと思います。

本の登録時のリクエストスペック

spec/requests/books_spec.rb
require 'rails_helper'

RSpec.describe 'Books', type: :request do
  it '適切なGoogle Books APIのIDである本が登録できること' do
    expect {
      post '/books', params: { googlebooksapi_id: 'xPbRxgEACAAJ' }
    }.to change { Book.count }.by(1)
    expect(response).to redirect_to book_path(Book.last.id)
  end

  it '既に登録されている本の登録に失敗し、その詳細画面に遷移すること' do
    same_google_books_api_id = 'xPbRxgEACAAJ'
    create(:book, googlebooksapi_id: same_google_books_api_id)
    expect {
      post '/books', params: { googlebooksapi_id: same_google_books_api_id }
    }.to change { Book.count }.by(0)
    expect(response).to redirect_to book_path(Book.find_by(googlebooksapi_id: same_google_books_api_id))
  end
end

対応するControllerは「リソース登録のController ver.3(完成)」のものです。
テストは説明することが少なくて楽で良いですね

以上になります。
改善していければと思っておりますので、間違ってる箇所や改善すべき箇所があればご指摘いただけると嬉しいです。

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6