←Rails 6で認証認可入り掲示板APIを構築する #3 RSpec, FactoryBot導入しpostモデルを作る
RSpecでmodelのテストを書く
とりあえずcreate, readが動くのは前回確認できました。
ここからはこの手順でいきます
- postのmodelテストを書く
- validationを実装する
- postのcontrollerテストを書く
- controller, routesを書く
- seedを書く
この記事ではとりあえず1.と2.の実装をして、3.以降はまた次回以降の記事で進めていきます。
rubocopをあらかじめつぶしておく
ドキュメンテーション書きなさいエラーがmigrationファイルにも出てくるので除外します。
.rubocop.ymlはこのように除外設定もできますが、本来必要なものも面倒がって除外していくとそもそもコーディング規約を守る意味が崩れるので、チーム開発で追加する際はしっかり議論しましょう。
+ # ドキュメンテーション
+ Style/Documentation:
+ Exclude:
+ - "db/migrate/**/*"
...
まずmodelテストを書く
テスト駆動開発(TDD)っぽく、まずはRedのテストです。
バリデーション未実装なのでテストを書いて動かしてもRedとなるものを作ります。
一旦factory_bot使わずに普通のRailsチックに書いてみます。
# 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を使わずに
# 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をエラーとするバリデーション
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というバリデーションを追加する予定でテストを書いてみます。
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を追加します。
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を作った時の初期値から特に変える必要はありませんが、一応中身を見ておきます。
# frozen_string_literal: true
FactoryBot.define do
factory :post do
subject { "MyString" }
body { "MyText" }
end
end
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に置き換える
とりあえず以下のように変更してみてください。
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実装回答例
# 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実行するとコケる
# 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やらコーディング規約やら遵守すると逆効果のこともあるので、あまり厳しくしないほうがいいです。
+ # ブロック長さ
+ Metrics/BlockLength:
+ Exclude:
+ - "spec/**/*"
この時点でrspec, rubocop実行すると通る
続き
→Rails 6で認証認可入り掲示板APIを構築する #5 controller, routes実装
【連載目次へ】