Post モデル作成
いよいよ、TechLog のメイン機能である学習ログを投稿・閲覧する機能を作成していきます。
本カリキュラムは Rails の基礎は学んでいることを前提としており、
基本的な機能の実装については細かい解説はしていませんのでご注意ください。
それよりもむしろ、どうやって機能を正確にテストするかという「実践的な開発」部分に着目していただければと思います。
モデル作成
TechLog では、学習ログの投稿一つひとつを Post
というモデルオブジェクトとして扱うことにします。
rails g
コマンドで Post モデル関連のファイルを作成しましょう。
$ bundle exec rails g model Post
invoke active_record
create db/migrate/20220727120112_create_posts.rb
create app/models/post.rb
invoke rspec
create spec/models/post_spec.rb
invoke factory_bot
create spec/factories/posts.rb
次は、作られたファイルからテーブルを準備(マイグレーション)します。
マイグレーション
マイグレーションファイルの編集
Post モデル、もとい posts テーブルに持たせたいカラム名をマイグレーションファイル内で定義します。
先ほど作成されたファイルのうち db/migrate/xxxxxxxxxx_create_posts.rb
を開き、以下のように編集してください。
class CreatePosts < ActiveRecord::Migration[7.0]
def change
create_table :posts do |t|
t.string :title, null: false # タイトルカラム
t.string :content, null: false # 本文カラム
t.references :user, foreign_key: true, null: false # 外部キー (User)
t.timestamps
end
end
end
Post はユーザーが作るものですので、必ずいずれかの User に紐づきます。
そのため、User を外部キーとして設定しています。
後ほど、Post モデルに User との紐付けを定義することで、Post に紐づく User、そして User に紐づく Post 情報を取得できるようにします。
マイグレーション実行
では、先ほど編集したマイグレーションファイルの内容をデータベースに反映していきましょう。
まずは development からです。
$ bundle exec rails db:migrate
== 20220727120112 CreatePosts: migrating ======================================
-- create_table(:posts)
-> 0.0015s
== 20220727120112 CreatePosts: migrated (0.0015s) =============================
同じく test の方もマイグレーションしましょう。
$ RAILS_ENV=test bundle exec rails db:migrate
== 20220727120112 CreatePosts: migrating ======================================
-- create_table(:posts)
-> 0.0007s
== 20220727120112 CreatePosts: migrated (0.0008s) =============================
エラーなく、マイグレーションが完了することを確認してください。
テスト作成
では、テスト駆動開発スタイルに従い、あるべき仕様を先にテストで定義しましょう。
以下のような動作を仕様として定義し、それぞれテストを追加していきます。
- Post モデル
- バリデーションの検証
- 正常系
- 正しいパラメータを渡せば
- 異常系
- title が空の場合は無効
- title が 100 文字を超える場合は無効
- content が空の場合は無効
- content が 1000 文字を超える場合は無効
- user_idが空の場合
- 正常系
- Post が持つ情報の検証
- 作成した Post が title を持つこと
- 作成した Post が content を持つこと
- 作成した Post から紐づく User 情報を取得できること
- バリデーションの検証
- User モデル
- 紐づく Post 情報を取得できること
では、テストを作成していきます。
FactoryBot の準備
Post モデルに関するテストをスムーズに作成するため、FactoryBot を用いてテスト内で Post を準備しやすくしておきます。
rails g
コマンドで作成された spec/factories/post.rb
を以下のように編集してください。
spec/factories/post.rb
FactoryBot.define do
factory :post do
title { 'タイトル1' }
content { '本文1' }
association :user, factory: :user
end
end
association :user, factory: :user
の1行により、同時に Post に紐づく User も作成できるように設定していることに注意してください。
これで、RSpec ファイル内で create(:post)
という1行だけで Post を作成できるようになりました。
次は、実際に Post モデルのテストを準備しましょう。
Post モデルのテスト
spec/models/post_spec.rb
require 'rails_helper'
describe Post do
before { @user = create(:user) } # 事前にユーザーを作成
let(:title) { 'テストタイトル' }
let(:content) { 'テスト本文' }
let(:user_id) { @user.id } # 作成したユーザーのIDを外部キーに設定
describe 'バリデーションの検証' do
let(:post) { Post.new(title: title, content: content, user_id: user_id) }
context '正常系' do
it '有効である' do
expect(post.valid?).to be(true)
end
end
context '異常系' do
context 'titleが空の場合' do
let(:title) { nil }
it '無効である' do
expect(post.valid?).to be(false)
expect(post.errors[:title]).to include('が入力されていません。')
end
end
context 'titleが100文字を超える場合' do
let(:title) { 'あ' * 101 }
it '無効である' do
expect(post.valid?).to be(false)
end
end
context 'contentが空の場合' do
let(:content) { nil }
it '無効である' do
expect(post.valid?).to be(false)
expect(post.errors[:content]).to include('が入力されていません。')
end
end
context 'contentが1000文字を超える場合' do
let(:content) { 'あ' * 1001 }
it '無効である' do
expect(post.valid?).to be(false)
end
end
context 'user_idが空の場合' do
let(:user_id) { nil }
it '無効である' do
expect(post.valid?).to be(false)
expect(post.errors[:user]).to include('が入力されていません。')
end
end
end
end
describe 'Postが持つ情報の検証' do
before { create(:post, title: title, content: content, user_id: user_id) } # Post を作成
subject { described_class.first }
it 'Postの属性値を返す' do
expect(subject.title).to eq('テストタイトル')
expect(subject.content).to eq('テスト本文')
expect(subject.user_id).to eq(@user.id)
end
end
end
以上が Post モデルのテストです。
バリデーションのエラーメッセージは日本語ですので、後ほど翻訳ファイル (config/locales/devise.views.ja.yml) も修正する必要があります。
User モデルのテスト
次は User モデルに紐づく Post を取得できる仕様を満たすテストを追加します。
User モデルのテストは作成済みですので、その中にテストを追加・修正してあげます。
spec/models/user_spec.rb
...
describe '.first' do
before do
@user = create(:user, nickname: nickname, email: email) # 修正
@post = create(:post, title: 'タイトル', content: '本文', user_id: @user.id) # 修正
end
subject { described_class.first }
it '事前に作成した通りのUserを返す' do
expect(subject.nickname).to eq('テスト太郎')
expect(subject.email).to eq('test@example.com')
end
####### ここから追加 #######
it '紐づくPostの情報を取得できる' do
expect(subject.posts.size).to eq(1)
expect(subject.posts.first.title).to eq('タイトル')
expect(subject.posts.first.content).to eq('本文')
expect(subject.posts.first.user_id).to eq(@user.id)
end
####### ここまで追加 #######
end
...
before ブロックの中で、User に紐づく Post を作成しています。
また、User は複数の Post を持つ場合があるので
User.post
ではなく User.posts
と、複数の Post を取得することに注意してください。
モデルの修正
では、テストが通るように Post モデルと User モデルを修正していきましょう.
Post モデルの修正
Post モデルを定義するファイルを以下のように修正します。
app/models/post.rb
class Post < ApplicationRecord
belongs_to :user
validates :title, presence: true, length: { maximum: 100 }
validates :content, presence: true, length: { maximum: 1000 }
end
belongs_to
により、Post に対して単一の User を紐付けています。
User モデルの修正
次に User モデルを定義するファイルを以下のように修正します。
app/models/user.rb
class User < ApplicationRecord
has_many :posts # 追加
devise :database_authenticatable, :registerable,
:recoverable, :rememberable, :validatable
validates :nickname, presence: true, length: { maximum: 20 }
end
has_many
により、User に対して複数の Post を紐付けています。
これでモデル自体の修正は完了です。
翻訳ファイル修正
先ほどテスト修正時に伝えた通り、バリデーション時のエラー内容に対する日本語訳を定義する必要があります。
翻訳ファイル config/locales/devise.views.ja.yml
を開き、ja.activerecord.errors.models.post
配下の日本語訳を定義します。
ja:
activerecord:
errors:
models:
user:
attributes:
email:
taken: "は既に使用されています。"
blank: "が入力されていません。"
invalid: "は有効でありません。"
nickname:
blank: "が入力されていません。"
too_long: "は%{count}文字以下に設定して下さい。"
password:
blank: "が入力されていません。"
too_short: "は%{count}文字以上に設定して下さい。"
too_long: "は%{count}文字以下に設定して下さい。"
invalid: "は有効でありません。"
password_confirmation:
confirmation: "が一致していません。"
post: # ここから追加
attributes:
title:
blank: "が入力されていません。"
too_long: "は%{count}文字以下に設定して下さい。"
content:
blank: "が入力されていません。"
too_long: "は%{count}文字以下に設定して下さい。"
user:
required: "が入力されていません。"
...
何かモデルを増やすたびに、このように user と同じ階層レベルで各属性のバリデーションメッセージを定義してあげる必要があります。
テスト実行
これで Post モデル、User モデル、そして翻訳ファイルの修正が完了したので仕様を満たしたはずです。
すべてのテストを実行し、コケるテストがないことを確認しましょう。
$ bin/rspec
...
Finished in 10.46 seconds (files took 0.49612 seconds to load)
37 examples, 0 failures
変更をコミット
これから学習ログを管理していく準備が完了しました。
ここまでの変更をコミットしておきましょう。
$ git add .
$ git commit -m "Postモデルを作成"
$ git push
宿題
アソシエーション
Post と User 間の関連付けには「アソシエーション」という仕組みを利用しています。
Post のマイグレーションファイル内で user に foreign_key: true
を含む1行を設定していましたが、
これがアソシエーションを利用することの宣言となります。
モデル間(テーブル間)の結びつけは中級者でも混乱しやすい分野ですので、整理して理解しておきましょう。
外部キーを含む FactoryBot
Post の Factory を定義した際、association
を設定することで Post に紐づく User も同時に準備できるようになっていました。
余裕があれば、association の仕組みについても把握しておきましょう。