Rails 6で認証認可入り掲示板APIを構築する #4 postのバリデーション、テスト実装

←[Rails 6で認証認可入り掲示板APIを構築する #3 RSpec, FactoryBot導入しpostモデルを作る](https://qiita.com/rf_p/items/9b7cc0b3a76d6141b103)

# RSpecでmodelのテストを書く

とりあえずcreate, readが動くのは前回確認できました。
ここからはこの手順でいきます

1. postのmodelテストを書く
2. validationを実装する
3. postのcontrollerテストを書く
4. controller, routesを書く
5. seedを書く

この記事ではとりあえず1.と2.の実装をして、3.以降はまた次回以降の記事で進めていきます。

# rubocopをあらかじめつぶしておく

ドキュメンテーション書きなさいエラーがmigrationファイルにも出てくるので除外します。

.rubocop.ymlはこのように除外設定もできますが、本来必要なものも面倒がって除外していくとそもそもコーディング規約を守る意味が崩れるので、チーム開発で追加する際はしっかり議論しましょう。

```diff:.rubocop.yml
+ # ドキュメンテーション
+ Style/Documentation:
+  Exclude:
+    - "db/migrate/**/*"
...
```

# まずmodelテストを書く

テスト駆動開発(TDD)っぽく、まずはRedのテストです。
バリデーション未実装なのでテストを書いて動かしてもRedとなるものを作ります。

一旦factory_bot使わずに普通のRailsチックに書いてみます。

```rb:spec/models/post_spec.rb
# frozen_string_literal: true

require "rails_helper"

RSpec.describe Post, type: :model do
  describe "subject" do
    context "blankの時に" do
      it "invalidになる" do
        post = Post.new(subject: "", body: "fuga")
        expect(post).not_to be_valid
      end
    end
  end
end
```

* describeはテスト対象を示します
* contextは条件を示します
* it(もしくはexample)はテスト対象を示します
* expect(post).not_to be_validは、postがbe_validと等しくないことをテストしています

なので上記コードは『subjectがblankの時にinvalidになる』ことをテストしています。
ですが今のところpostモデルのsubjectにバリデーションを入れていないのでpostは有効であり、「invalidになる」テストは失敗することになります。

```powershell
$ rspec spec/models/post_spec.rb
...
Finished in 0.07805 seconds (files took 3.53 seconds to load)
1 example, 1 failure

Failed examples:

rspec ./spec/models/post_spec.rb:8 # Post subject blankの時に invalidになる

ec2-user:~/environment/bbs (master) $ rspec
```

本当にsubjectが空でも登録できるか試してみましょう。

```powershell
$ rails c
[1] pry(main)> Post.create!(subject: "", body: "hoge")
   (0.1ms)  BEGIN
  Post Create (2.5ms)  INSERT INTO "posts" ("subject", "body", "created_at", "updated_at") VALUES ($1, $2, $3, $4) RETURNING "id"  [["subject", ""], ["body", "hoge"], ["created_at", "2020-09-06 01:07:52.628768"], ["updated_at", "2020-09-06 01:07:52.628768"]]
   (0.9ms)  COMMIT
=> #<Post:0x0000000005760700
 id: 2,
 subject: "",
 body: "hoge",
 created_at: Sun, 06 Sep 2020 01:07:52 UTC +00:00,
 updated_at: Sun, 06 Sep 2020 01:07:52 UTC +00:00>
```

保存できてしまいましたね。

ちなみにdescribeやcontextを使わずに

```rb:spec/models/post_spec.rb
# frozen_string_literal: true

require "rails_helper"

RSpec.describe Post, type: :model do
  it "subjectがblankの時にinvalidになる" do
    post = Post.new(subject: "", body: "fuga")
    expect(post).not_to be_valid
  end
end
```

というコードでもほぼ同じテストコードの挙動になります。なぜなら、itブロックでexpectを書けばテストができるためです。
しかし同カラムや同バリデーション条件等をグルーピングして記述する際に分かりづらくなるので、基本的にdescribeやcontextを入れ子にして記述するのがオススメです。

# modelにバリデーションを追加する

## blankをエラーとするバリデーション

```diff:app/models/post.rb
 class Post < ApplicationRecord
+  validates :subject, presence: true
 end
```

これで、subjectカラムに対してpresence(存在)に対してtrue, つまりblankでの登録ができなくなります。

試してみましょう。

```powershell
$ rails c
[1] pry(main)> Post.create!(subject: "", body: "hoge")
ActiveRecord::RecordInvalid: Validation failed: Subject can't be blank
from /home/ec2-user/.rvm/gems/ruby-2.7.1/gems/activerecord-6.0.3.2/lib/active_record/validations.rb:80:in `raise_validation_error'
```

登録できないですね。

```powershell
$ rspec ./spec/models/post_spec.rb 
...
Finished in 0.05053 seconds (files took 1.63 seconds to load)
1 example, 0 failures
```

テストも通過しました。

## 最大文字数のバリデーション

文字数が無限に登録できると困るので制限を加えます。
こちらも先にテストから。
30文字以内ならOK、31文字以上はNGというバリデーションを追加する予定でテストを書いてみます。

```diff:spec/models/post_spec.rb
         expect(post).not_to be_valid
       end
     end
+    context "maxlengthにより" do
+      context "30文字の場合に" do
+        it "validになる" do
+          post = Post.new(subject: "あ" * 30, body: "fuga")
+          expect(post).to be_valid
+        end
+      end
+      context "31文字の場合に" do
+        it "invalidになる" do
+          post = Post.new(subject: "あ" * 31, body: "fuga")
+          expect(post).not_to be_valid
+        end
+      end
+    end
   end
 end
```

テスト実行してみましょう。

```powershell
$ rspec ./spec/models/post_spec.rb 
...
Finished in 0.03204 seconds (files took 1.42 seconds to load)
3 examples, 1 failure

Failed examples:

rspec ./spec/models/post_spec.rb:21 # Post subject maxlengthにより 31文字の場合に invalidになる
```

まだvalidationを追加していないので、30文字はパスしますが31文字はコケますね。
modelにvalidationを追加します。

```diff:app/models/post.rb
 class Post < ApplicationRecord
-  validates :subject, presence: true
+  validates :subject, presence: true, length: { maximum: 30 }
 end
```

```powershell
$ rspec ./spec/models/post_spec.rb 
...
Finished in 0.02201 seconds (files took 1.4 seconds to load)
3 examples, 0 failures
```

テスト通りましたね。
これで31文字の際にエラーになります。`rails c`で試してみると良いでしょう。

# FactoryBotに置き換える

例えば先程のテストコードにある

```rb
  post = Post.new(subject: "あ" * 30, body: "fuga")
```

ですが、毎回body指定するの面倒ですよね。
2カラムであればまだ大丈夫ですが、これが10カラムとか超えてくると無駄にコードが長くなります。
その際にfactoryBotを使います。

factoryBotはspec/factories/下を参照します。
今回はmodelを作った時の初期値から特に変える必要はありませんが、一応中身を見ておきます。

```spec/factories/posts.rb
# frozen_string_literal: true

FactoryBot.define do
  factory :post do
    subject { "MyString" }
    body { "MyText" }
  end
end
```

post_spec.rbファイルを編集します。

```diff:spec/models/post_spec.rb
   describe "subject" do
     context "blankの時に" do
       it "invalidになる" do
-        post = Post.new(subject: "", body: "fuga")
+        post = build(:post, subject: "")
         expect(post).not_to be_valid
       end
     end
     context "maxlengthにより" do
       context "30文字の場合に" do
         it "validになる" do
-          post = Post.new(subject: "あ" * 30, body: "fuga")
+          post = build(:post, subject: "あ" * 30)
           expect(post).to be_valid
         end
       end
       context "31文字の場合に" do
         it "invalidになる" do
-          post = Post.new(subject: "あ" * 31, body: "fuga")
+          post = build(:post, subject: "あ" * 31)
           expect(post).not_to be_valid
         end
       end
```

buildはfactoryBotを使った`.new`に相当するものです。データベースへの保存は行われません。
今回の場合subjectを指定していますがbodyは未指定なので、factoryBotのbodyは`"MyText"`が入ります。

また、変更のたびにテスト実行してOKになることを確認してください。

# 変数をletに置き換える

とりあえず以下のように変更してみてください。

```diff:spec/models/post_spec.rb
 RSpec.describe Post, type: :model do
   describe "subject" do
     context "blankの時に" do
+      let(:post) do
+        build(:post, subject: "")
+      end
       it "invalidになる" do
-        post = build(:post, subject: "")
         expect(post).not_to be_valid
       end
     end
     context "maxlengthにより" do
       context "30文字の場合に" do
+        let(:post) do
+          build(:post, subject: "あ" * 30)
+        end
         it "validになる" do
-          post = build(:post, subject: "あ" * 30)
           expect(post).to be_valid
         end
       end
       context "31文字の場合に" do
+        let(:post) do
+          build(:post, subject: "あ" * 31)
+        end
         it "invalidになる" do
-          post = build(:post, subject: "あ" * 31)
           expect(post).not_to be_valid
         end
       end
```

letは同一describeやcontextのブロック内のスコープに限定される変数です。
Rubyは最後に評価された式が返り値となるので、

```rb
  let(:post) do
    build(:post, subject: "あ" * 31)
  end
```

の場合は、build実行結果のpostが、`let(:post)`によってpostという変数になります。

# 演習

bodyにも必須制限・100文字以内制限のテストとバリデーションを実装してみましょう。

<details><summary>body実装回答例</summary><div>

```rb:spec/models/post_spec.rb
# frozen_string_literal: true

require "rails_helper"

RSpec.describe Post, type: :model do
  describe "subject" do
    context "blankの時に" do
      let(:post) do
        build(:post, subject: "")
      end
      it "invalidになる" do
        expect(post).not_to be_valid
      end
    end
    context "maxlengthにより" do
      context "30文字の場合に" do
        let(:post) do
          build(:post, subject: "あ" * 30)
        end
        it "validになる" do
          expect(post).to be_valid
        end
      end
      context "31文字の場合に" do
        let(:post) do
          build(:post, subject: "あ" * 31)
        end
        it "invalidになる" do
          expect(post).not_to be_valid
        end
      end
    end
  end

  describe "body" do
    context "blankの時に" do
      let(:post) do
        build(:post, body: "")
      end
      it "invalidになる" do
        expect(post).not_to be_valid
      end
    end
    context "maxlengthにより" do
      context "100文字の場合に" do
        let(:post) do
          build(:post, body: "あ" * 100)
        end
        it "validになる" do
          expect(post).to be_valid
        end
      end
      context "101文字の場合に" do
        let(:post) do
          build(:post, body: "あ" * 101)
        end
        it "invalidになる" do
          expect(post).not_to be_valid
        end
      end
    end
  end
end
```

この時点でrspec実行するとコケる

```rb:app/models/post.rb
# frozen_string_literal: true

#
# 投稿クラス
#
class Post < ApplicationRecord
  validates :subject, presence: true, length: { maximum: 30 }
  validates :body, presence: true, length: { maximum: 100 }
end
```

rubocopがコケるので除外設定。testはDRYやらコーディング規約やら遵守すると逆効果のこともあるので、あまり厳しくしないほうがいいです。

```diff:.rubocop.yml
+ # ブロック長さ
+ Metrics/BlockLength:
+   Exclude:
+     - "spec/**/*"
```

この時点でrspec, rubocop実行すると通る
</div></details>

# 続き

→[Rails 6で認証認可入り掲示板APIを構築する #5 controller, routes実装](https://qiita.com/rf_p/items/2b40f08e0509fb8ff56b)

【[連載目次へ](https://qiita.com/rf_p/items/a846553c11571f4177eb#%E7%9B%AE%E6%AC%A1)】