2
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

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

Last updated at Posted at 2020-09-09

Rails 6で認証認可入り掲示板APIを構築する #3 RSpec, FactoryBot導入しpostモデルを作る

RSpecでmodelのテストを書く

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

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

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

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

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

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

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

まずmodelテストを書く

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

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

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になる」テストは失敗することになります。

$ 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が空でも登録できるか試してみましょう。

$ 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を使わずに

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をエラーとするバリデーション

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

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

試してみましょう。

$ 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'

登録できないですね。

$ 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というバリデーションを追加する予定でテストを書いてみます。

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

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

$ 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を追加します。

app/models/post.rb
 class Post < ApplicationRecord
-  validates :subject, presence: true
+  validates :subject, presence: true, length: { maximum: 30 }
 end
$ 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に置き換える

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

  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ファイルを編集します。

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に置き換える

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

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は最後に評価された式が返り値となるので、

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

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

演習

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

body実装回答例
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実行するとコケる

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やらコーディング規約やら遵守すると逆効果のこともあるので、あまり厳しくしないほうがいいです。

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

この時点でrspec, rubocop実行すると通る

続き

Rails 6で認証認可入り掲示板APIを構築する #5 controller, routes実装

連載目次へ

2
3
0

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
2
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?