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

## 対象読者

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

## 本記事の到達物

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

```ruby
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"
```

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

```ruby
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
```

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

```ruby

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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/496036/731c9934-4ad7-132f-0cca-bffcef405623.png)

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

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

また、「Google Books APIから情報を取得するなら、自前のデータベースに情報を持つ必要は無いのでは？」と思われるかもしれません。
その設計を行い、失敗した話を以下に載せておきます。
[GoogleBooksAPIだけで本リソースの取得をする設計を行い、失敗した話](https://qiita.com/kumackey/items/3be24f6bc5f6f66515a0)

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


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

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

#####　Booksテーブル

```ruby: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テーブル

```ruby: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/`配下に追加します。

```ruby: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](https://developers.google.com/books/docs/v1/getting_started?hl=ja)

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

```ruby:Gemfile
gem 'addressable'
```

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

## 情報を格納するモデル

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

```ruby: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

```ruby
  include ActiveModel::Model
  include ActiveModel::Attributes
  include ActiveModel::Validations
```

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

[Active Model の基礎 - Railsガイド](https://railsguides.jp/active_model_basics.html)

#### ActiveModelのattribute

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

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

[ActiveModel::Attributes が最高すぎるんだよな。](https://qiita.com/alpaca_taichou/items/bebace92f06af3f32898)

#### ActiveModelのvalidates

```ruby
  validates :googlebooksapi_id, presence: true
  validates :title, presence: true
```

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

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

```ruby
  class << self
```

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

[Rubyのクラスメソッドをclass << selfで定義している理由（翻訳）](https://techracho.bpsinc.jp/hachi8833/2017_12_21/49467)

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

```ruby
    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`からインスタンスを生成する

```ruby
    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`の実装

```ruby
    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`下で定義します。

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

```ruby
    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つかと思います。

#### ルーティング

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

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

#### 検索画面のController

```ruby: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に送る](https://qiita.com/kumackey/items/4f8f0cd39d02299d74ed)

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

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

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

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

- `GoogleBook`モデルに持っていた情報を`Book`モデルや`Author`モデルに格納し直す
- ヴァリデーションをかける
- ヴァリデーション成功時にはDBに保存する

```ruby: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`メソッド

```ruby: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
```

`ActiveRecord`の`save`メソッド風に仕上げるため、保存に成功すれば`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(重複の登録をしてしまう)

```ruby: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`メソッドとして実装します。

```ruby: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(完成)

```ruby: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を叩くメソッドのテスト

```ruby: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】初心者でもメソッドの単体テストがしたい！！](https://qiita.com/yama-t/items/491f24124578a8e35764)

#### `GoogleBook`のモデルテスト

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

```ruby: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です。
次にモデルテストの本体です。

```ruby: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
```

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

#### 本の登録時のリクエストスペック
```ruby: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(完成)」のものです。
~~テストは説明することが少なくて楽で良いですね~~

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