RSpecによるTDDでRailsAPIを実装してみた。part1 -認証なしのアクション実装-

## はじめに

この記事は自分のための記憶を整理したりまとめたいという理由で書く記事です。もちろん読まれても恥ずかしく無いように書くますが、あくまでも自分で整理できればそれでOKな記事でありますので、説明不足ということがあるかと思いますが、あしからず...。

## 記事の内容

- この記事はTDDの形で開発をすめていきます。テストフレームワークはRSpecを使っています。
- 簡単なArticleとUserのモデルを使った記事管理システムを作っていきます。
- 認証はgithubAPIを使ったOAuth認証を用います。
- databaseはapiに集中するためにデフォルトのsqlite3を使います
- part1はindexアクションの実装までを目標に進めていきます
- この記事は長くなります。

```terminal
# versions
ruby 2.5.1
Rails 6.0.3.1
RSpec 3.9
  - rspec-core 3.9.2
  - rspec-expectations 3.9.2
  - rspec-mocks 3.9.1
  - rspec-rails 4.0.1
  - rspec-support 3.9.3
```

## 開発の準備

```terminal:terminal
$ rails new api_name -T --api
```

-T　はminitestに関わるファイルを生成しない
--api はこのプロジェクトはapiのためだけに使いますという宣言を前もってすることで、apiのためのファイル構成で始めることができる(ビューファイルが生成されないなど)

```ruby:gemfile

gem 'rspec-rails'              # rspecのgem
gem 'factry_bot_rails'         # テスト用のデータを入れるgem
gem 'active_model_serializers' # serializeするためのgem
gem 'kaminari'                 # pagenateするためのgem
gem 'octokit', "~> 4.0"        # githubユーザーを扱うためのgem
```


以上のgemをdevelopmentの中に挿入

`bundle install`

これでいったん開発の準備は終わる

## では開発していく

まず全体的な流れをなぞっておくと、
indexアクションの実装
↓
認証関係の実装(userモデルの実装)
↓
createアクションの実装
↓
updateアクションの実装

という流れで進めていく

## articleモデルを作成

```
$ rails g model title:string content:text slug:string
```

こんな構成のモデルを作成する

そして先ほど作成された`models/article_spec.rb`にテストコードを記述していく

```ruby:models/article_spec.rb
require 'rails_helper'

RSpec.describe Article, type: :model do
  describe '#validations' do
    it 'should validate the presence of the title' do
      article = build :article, title: ''
      expect(article).not_to be_valid
      expect(article.errors.messages[:title]).to include("can't be blank")
    end
  end
end
```

このテストの内容はもしもタイトルが空だった時にエラーメッセージを出す、というものと、もう一つはvalidationに引っかかるというテスト

`$ rspec spec/models/article_spec.rb`

を実行し、テストを動かす

その後エラーが出ることを確認する
今回の場合は

`undefined method build`

というようなエラーが出る、これはfactory_botが動いていない証拠なので、以下を追加

```ruby:rails_helper.rb:42
config.include FactoryBot::Syntax::Methods
```

その後再びrspecを実行
`$ rspec spec/models/article_spec.rb`

```terminal:failure
expected #<Article id: nil, title: "", content: "MyText", slug: "MyString", created_at: nil, updated_at: nil> not to be valid
```

validationに引っかからずにtitleがnilで保存されてしまっていることがわかる
なので、実際にmodelにvalidationを記述していく

```ruby:app/models/article.rb
class Article < ApplicationRecord
  validates :title, presence: true
end
```

再度テストを実行して通る

そしてこれをcontentとslugも同じように記述していく

```ruby:models/article_spec.rb
    it 'should validate the presence of the content' do
      article = build :article, content: ''
      expect(article).not_to be_valid
      expect(article.errors.messages[:content]).to include("can't be blank")
    end

    it 'should validate the presence of the slug' do
      article = build :article, slug: ''
      expect(article).not_to be_valid
      expect(article.errors.messages[:slug]).to include("can't be blank")
    end
```

```ruby:app/models/article.rb
  validates :content, presence: true
  validates :slug, presence: true
```

テストを実行し全て通る

```
3 examples, 0 failures
```

そしてもう一つvalidationをかけておきたいのが、slugが一意かどうか

なので、uniqueに関するテストも追加で記述

```ruby:models/article_spec.rb
    it 'should validate uniqueness of slug' do
      article = create :article
      invalid_article = build :article, slug: article.slug
      expect(invalid_article).not_to be_valid
    end
```

一度articleをcreateで作り再びbuildでarticleを作る。そして二つ目のarticleには一つ目のarticleのslugを指定しているので、二つのarticleのslugは同じものになる。これでテストをする。

ちなみにcreateとbuildの違いはデータベースに保存するかどうかで、それによって用途は変わってくる。
また、createを全てに使ってしまうと、重くなってしまうので、データベースに保存する必要のない（生成後直接expectで判定される）ものなどはbuildを使いメモリの消費をできるだけ減らす。

テストを実行しエラーが出るのを確認

```terminal:failure
expected #<Article id: nil, title: "MyString", content: "MyText", slug: "MyString", created_at: nil, updated_at: nil> not to be valid
```

そのままuniqueではないのに保存されてしまっているのがわかる
なので、modelを記述していく

```ruby:app/models/article.rb
validates :slug, uniqueness: true
```

その後テストは全てクリアする
これでarticleモデルのテストは終わり

## articles#index

次はコントローラーを実装していく、まずはルーティングからテストしていく

### articles#index routing

`spec/routing/`を作成
その後`spec/routing/articles_spec.rb`を作成

中身を記述していく

```ruby:spec/routing/articles_spec.rb
require 'rails_helper'

describe 'article routes' do
  it 'should route articles index' do
    expect(get '/articles').to route_to('articles#index')
  end
end
```

これはrouteが正しく動いているかを確認するテスト

実行するとエラーが出る

`No route matches "/articles"
`

`/articles`のルーティングは存在していないというエラーが出る
なのでルーティングを追加する


```routes.rb
resources :articles, only: [:index]
```

テストを実行するがエラーが出る

```terminal:failure
 A route matches "/articles", but references missing controller: ArticlesController
```

これは/articleというルートは存在しているが、articlescontrollerに当てはまるものが存在していないということなので、実際にコントローラーに記述していく


```
$ rails g controller articles
```

controllerを作成
中身を記述していく

```ruby:app/controllers/articles_controller.rb
def index; end
```

テストがとおる

ついでにshowアクションも実装していく

```ruby:spec/routing/articles_spec.rb
  it 'should route articles show' do
    expect(get '/articles/1').to route_to('articles#show', id: '1')
  end
```

```routes.rb
resources :articles, only: [:index, :show]
```

```articles_controller.rb
  def show
  end
```

そしてテストを実行し、通ることを確認する。

### articles#index 実装

次に実際にcontrollerの中身を実装していく

まずはテストから書いていく

spec/controllersというファイルを作りその中に`articles_controller_spec.rb`というファイルを作っていく

```ruby:spec/controllers/articles_controller_spec.rb
require 'rails_helper'

describe ArticlesController do
  describe '#index' do
    it 'should return success response' do
      get :index
      expect(response).to have_http_status(:ok)
    end
  end
end
```

このテストは単純に、get :indexのリクエストを送り、200番が返って来ることを期待するというテスト。:okは200と同じ意味。

そのままテストを実行する。

`expected the response to have status code :ok (200) but it was :no_content (204)`

というメッセージが出て、テストが失敗する。
このメッセージは200を期待していたが204が返ってきたことを意味する。
204は何も返ってこなかった、ということなので、大体はdeleteやupdateなどのレスポンスで利用される。しかし今回は200が返ってきて欲しいので、controllerを編集していく。

```ruby:app/controllers/articles_controller.rb
  def index
    articles = Article.all
    render json: articles
  end
```
内容はシンプルで、データベースから全てのarticleを取り出し、それをrenderで返すという処理をしている。
json形式で返すために`json:`という書き方をしている
ちなみに、`render articles`とそのまま返しても200が返るのでこのテストは成功する。json形式ではないresponseは好ましいとは言えない。後にserializerを使ってresponseを解析していくが、その時にはやはりjsonに変換しておく必要があるので json:はつけ忘れないようにする。

それではjson形式を確認するtestを書いていく

```ruby:spec/controllers/articles_controller_spec.rb
    it 'should return proper json' do
　　 　create_list :article, 2
      get :index
      json = JSON.parse(response.body)
      json_data = json['data']
      expect(json_data.length).to eq(2)
      expect(json_data[0]['attributes']).to eq({
        "title" => "My article 1",
        "content" => "The content of article 1",
        "slug" => "article-1",
      })
    end
```

いったんこのテストを実行する。説明は必要な時に書いていく。

`Validation failed: Slug has already been taken`

まずこのエラーが出る。slugはunique出ないとvalidationにかかってしまう。なのでfactory botを編集して一つひとつのarticleをuniqueにする。

```ruby:spec/factories/articles.rb
FactoryBot.define do
  factory :article do
    sequence(:title) { |n| "My article #{n}"}
    sequence(:content) { |n| "The content of article #{n}"}
    sequence(:slug) { |n| "article-#{n}"}
  end
end
```
このようにsequenceを使えば、一つ一つのarticleの名前をuniqueにすることができる。

これで実行してみる。

```
Failure/Error: json_data = json[:data]
no implicit conversion of Symbol into Integer
```
このようなメッセージが出て失敗する。これは[:data]が存在しないことによるエラーで、この[:data]はserializerを使って形式を変換したときのものなので、serializerを使って、jsonの形式を変換していく。

最初にあらかじめ
`gem 'active_model_serializers'`という記述をしてgemを入れているので、それを使って、記述していく。

まずはserializer用のファイルを作っていく。

```
$ rails g serializer article title content slug
```

これにより、`app/serializers/article_serializer.rb`が作成される。

そしてその新しく導入したactive_model_serializerを適応させるために新しく記述する。

```ruby:config/initializers/active_model_serializers.rb
ActiveModelSerializers.config.adapter = :json_api
```
このファイルを作り、記述を追加することで新しく導入したserializerを適応させる。

これにより、defaultで使われていたものを変更することができ、[:data]などで取り出すことができる形式に変換することができる。

それぞれのレスポンスの違いを見ていく。

ActiveModel::Serializer導入前

```
JSON.parse(response.body)
=> [{"id"=>1,
  "title"=>"My article 1",
  "content"=>"The content of article 1",
  "slug"=>"article-1",
  "created_at"=>"2020-05-19T06:22:49.045Z",
  "updated_at"=>"2020-05-19T06:22:49.045Z"},
 {"id"=>2,
  "title"=>"My article 2",
  "content"=>"The content of article 2",
  "slug"=>"article-2",
  "created_at"=>"2020-05-19T06:22:49.049Z",
  "updated_at"=>"2020-05-19T06:22:49.049Z"}]
```

ActiveModel::Serializer導入後

```
JSON.parse(response.body)
=> {"data"=>
  [{"id"=>"1",
    "type"=>"articles",
    "attributes"=>
     {"title"=>"My article 1", "content"=>"The content of article 1", "slug"=>"article-1"}},
   {"id"=>"2",
    "type"=>"articles",
    "attributes"=>
     {"title"=>"My article 2", "content"=>"The content of article 2", "slug"=>"article-2"}}]}
```

中の構造が変わっていることがわかる。また、serializerで指定していない、created_atやupdated_atの属性は切り捨てられている。

これで再びテストを実行すると成功する。

成功したものの重複した表現が多いので、少しリファクタリングをする。

`get :index`だが、これは複数回送ることが多いので、まとめて一回で定義する。

```ruby
describe '#index' do
  subject { get :index }
```
このように記述することで、まとめて記述できる。
その定義を使うためには定義した場所から下で`subject`と打つだけで使うことができる。
仮に、その下の階層でもう一度定義された場合は、そのあとから定義した方が使われる。

それを使い二箇所にsubjectと置き換えることができる。

```ruby
    it 'should return proper json' do
      articles = create_list :article, 2
      subject
      json = JSON.parse(response.body)
      json_data = json['data']
      articles.each_with_index do |article, index|
        expect(json_data[index]['attributes']).to eq({
          "title" => article.title,
          "content" => article.content,
          "slug" => article.slug,
        })
      end
```

そして、この部分はeach_with_indexを使うことで、重複した表現をまとめることができる。

そしてさらに

```
json = JSON.parse(response.body)
json_data = json['data']
```

この二つの記述は繰り返し使うことが多いので、helperメソッドに定義しておく。

`spec/support/`を作成し、その下に`json_api_helpers.rb`を作成する。

```ruby:spec/support/json_api_helpers.rb
module JsonApiHelper
  def json
    JSON.parse(response.body)
  end

  def json_data
    json["data"]
  end
end
```

これを全てのファイルで扱えるようにするために、spec_helper.rbでincludeさせておく。

```spec/rails_helper.rb
config.include JsonApiHelpers
```
そしてsupportを読み込ませるようにしたの記述のコメントアウトを外しておく。

```spec/rails_helper.rb
Dir[Rails.root.join('spec', 'support', '**', '*.rb')].sort.each { |f| require f }
```

これにより、以前の記述を省略して書ける。

```ruby:spec/controllers/articles_controller_spec.rb
    it 'should return proper json' do
      articles = create_list :article, 2
      subject
      articles.each_with_index do |article, index|
        expect(json_data[index]['attributes']).to eq({
          "title" => article.title,
          "content" => article.content,
          "slug" => article.slug,
        })
      end
```

これで、indexはほぼ完成した。しかし、記事の順番が最新が最後になってしまう。最新は最初に来るように、sortを設定する。

まずは期待するテストから書く。

```ruby:spec/controllers/articles_controller_spec.rb
    it 'should return articles in the proper order' do
      old_article = create :article
      newer_article = create :article
      subject
      expect(json_data.first['id']).to eq(newer_article.id.to_s)
      expect(json_data.last['id']).to eq(old_article.id.to_s)
    end
```
以上の記述を追記する。
2度articleをcreateし、古い方と新しい方を作る。
そして、それらがjson_dataの何番目かで、最新が、先に来ているかどうかを判定する。
to_sメソッドを使っているのはserializerを使うとvalueの全てが文字列に変換されるので、to_sメソッドを使って、factorybotで生成したデータは文字列にしなければいけない。idも文字列に変換されているので注意が必要。

これでテストを実行する。
`rspec spec/controllers/articles_controller_spec.rb`

```terminal:failure
expected: "2"
got: "1"
```

このようなメッセージが出力される。
まだ、sortを何も触っていないので、当たり前だが、最後のarticleは最初に来ている。

sortを実装したいのだが、メソッドとしてmodelに記述したいのでmodelのテストをまず先に書く。
メソッド名は.recentとする。

```ruby:spec/models/article_spec.rb
  describe '.recent' do
    it 'should list recent article first' do
      old_article = create :article
      newer_article = create :article
      expect(described_class.recent).to eq(
        [ newer_article, old_article ]
      )
    end
  end
```

これはcontrollerのテストと似たようなことをしているが、違うのはcontrollerとは分離していて、メソッドを呼び出す処理のみにフォーカスしていること。要するにこのテストのみで完結する。

`described_class.method_name`
これにより、クラスメソッドを呼び出すことができる。
この場合のdescribed_classはArticleとなるが記述するファイルにより、そこに何が入るかは変わって来る。

あと、updateした時に古い記事が最近に来るかどうかもテストしたい。
なので以下を追記する。

```ruby:spec/models/article_spec.rb
    it 'should list recent article first' do
      old_article = create :article
      newer_article = create :article
      expect(described_class.recent).to eq(
        [ newer_article, old_article ]
      )

      old_article.update_column :created_at, Time.now
      expect(described_class.recent).to eq(
        [ old_article, newer_article ]
      )
    end
```

テストを実行すると

`undefined method recent`と出るので、recentを定義していく。

これまでrecentをメソッドとして定義するというふうに書いていたが、scopeの方が用途に合っているのでscopeで実装する。
scopeはほとんどの場合、classメソッドと同じように扱われる。

長々とテストを書いたが、実装はすぐ終わる。

```ruby:app/models/article.rb
scope :recent, -> { order(created_at: :desc) }
```
一行を追加してscopeを定義する。

そして、テストを実行するmodelのテストは全てがグリーンになる。
`rspec spec/models/article_spec.rb`

しかし、controllerの方はいくつか失敗する。
`rspec spec/controllers/articles_controller_spec.rb`
それはrecentを定義しただけで、controllerdで使ってないからなので、実際にcontrollerに記述していく。

```ruby:app/controllers/articles_controller.rb
articles = Article.recent
```
Article.allをArticle.recentに変更する。

これでもう一度テストを実行する。
`rspec spec/controllers/articles_controller_spec.rb`

すると、test側でsortできていなくて失敗するので、test側もsortする。

```ruby:spec/controllers/articles_controller_spec.rb
 Article.recent.each_with_index do |article, index| # 14行目
```
このようにarticles.recentではなく、Article.recentと記述したのは、articlesはfactory_botの生成したものなので、実際のArticleのインスタンスではないので、.recentが使えないため。

直接factorybotで作成したものを利用する必要がなくなったので、
`articles = create_list :article, 2`
この記述は
`create_list :article, 2`
このようにcreateするだけにしておく。

これでテストを実行すれば成功する。

paginationを次は実装する。
paginationとは一ページあたり幾つの記事というふうに指定できるようにするもの。

まずはいつものようにテストから書いていく。

```ruby:spec/controllers/articles_controller_spec.rb
    it 'should paginate results' do
      create_list :article, 3
      get :index, params: { page: 2, per_page: 1 }
      expect(json_data.length).to eq 1
      expected_article = Article.recent.second.id.to_s
      expect(json_data.first['id']).to eq(expected_article)
    end
```

この行を追加。paramsに新しいparameterを指定している。
pageは何ページ目かを指定していて、per_pageは一ページあたり、幾つのarticleかを指定している。今回の場合は、2ページ目が欲しくて、そのページに記載されているのは1つのarticleということになる。

テストを実行する。

```terminal:failure
expected: 1
got: 3
```

一つしか返ってこないことを期待したが、全てのarticleが返ってきてしまった。なので、ここからpaginationを実装していく。

この記事の一番初めにkaminariというgemは既に追加している。kaminariはpaginationを簡単に実現することができるgem。

なのでそのgemを使って実装していく。

```ruby:app/controllers/articles_controller.rb
    articles = Article.recent.
      page(params[:page]).
      per(params[:per_page])
```

recentに続けて、追記する。このように、orマッパーのように、.pageや.perを使うことができる。そしてその引数にparamsで送られてきた値を挿入すれば良い。

これで、responseを絞り込むことができ、一度のresponseによって返すデータ量を指定することができる。

これでもう一度テストを実行すれば成功する。

いったんここで予定していたpart1は終わり。indexの実装は完了。

長い記事にお付き合いいただきありがとうございます。お疲れ様でした。

## pert2投稿しました。

[RSpecによるTDDでRailsAPIを実装してみた。part2](https://qiita.com/yoshi_4/items/963bd1f5397caf8d7d67)