こんにちは、株式会社Pentagonでアプリ開発をしている中原です。
Railsのテストを書く方法について調査することがあり、どのようにテストを書いて良いか、分からなかったためその方法を記事にしました。本記事では、テストフレームワークRSpecを使用したモデルの単体テストの書き方について説明します。
【こんな人に読んで欲しい】
- Railsエンジニア
- テストを書く方法が分からない方
【この記事を読むメリット】
- テストを書く方法について学ぶことができる。
- RSpecを使用した単体テストを書く方法について学ぶことができる。
【結論】
テストを書く方法はgiven、 when、 thenのフレームワーク通りにテストコードを書くことです。
前回の記事から環境構築を行い、本記事を読み、実装を行えば、RSpecを使用したモデルの単体テストを書くことができます。RSpecを使用することで、テストコードの可読性を向上させ、より簡単にテストを書くことができます。
前提条件
前回の記事から環境構築を行なってください。
また以下の文献を参考にRidgepoleの導入を行なってください。
- Ridgepoleの導入
参考
【Rails】migrationからRidgepoleに移行する
https://zenn.dev/nyazuki/articles/28d1d4e4ebef05
RSpecとは
RSpecは、RubyベースのBDD(行動駆動開発)テスティングフレームワークです。BDDとは、開発者がアプリケーションの振る舞いを記述し、それに対するテストケースを作成することで、アプリケーションの品質を向上させる手法です。RSpecは、自然言語に近い構文を使用してテストケースを記述するため、テストコードの可読性を向上させ、開発者がテストコードを簡単に理解することができます。またテストケースの実行結果をグラフィカルに表示できるため、テストケースの実行結果を直感的に理解することができます。
テストに必要なGemを導入
group :development, :test do
# テストフレームワーク
gem 'rspec-rails'
# テスト用オブジェクトの生成
gem 'factory_bot_rails'
# ダミーデータの作成
gem "faker"
######### 省略 #########
gem "debug", platforms: %i[ mri mingw x64_mingw ]
end
追加したGemをインストール
docker-compose exec web bundle install
RSpecのセットアップ
docker-compose exec web rails g rspec:install
以下のファイルが作成されます。
identical .rspec
create spec # specの設定ファイル
create spec/spec_helper.rb # Railsの設定ファイル
create spec/rails_helper.rb # RSpecの全体的な設定ファイル
.rspeファイルの編集
--require spec_helper
--color # 追加
--format documentation # 追加
テスト実行時の出力がドキュメント形式や色付けされ可読性が高まります。
FactoryBotの設定
require 'spec_helper'
ENV['RAILS_ENV'] ||= 'test'
######### 省略 #########
# コメントアウトを外す
Dir[Rails.root.join('spec', 'support', '**', '*.rb')].sort.each { |f| require f }
######### 省略 #########
RSpec.configure do |config|
######### 省略 #########
config.include FactoryBot::Syntax::Methods # 追加
end
テスト用オブジェクトを生成時にFactoryBot
を省略してcreate()
やbuild()
と書くことができます。
FactoryBot.build(title: '', content: '')
↓
build(title: '', content: '')
自動生成ファイル設定
module App
class Application < Rails::Application
config.load_defaults 7.0
config.api_only = true
######## 省略 ########
# ここから
config.generators do |g|
g.test_framework :rspec, # テストフレームワークとしてRSpecを指定
request_specs: false, # リクエストスペックを作成しない
fixtures: false, # テストデータを作るfixtureを作成しない
view_specs: false, # ビュー用のスペックを作成しない
helper_specs: false, # ヘルパー用のスペックを作成しない
routing_specs: false # ルーティングのスペックを作成しない
end
# ここまでを追加
end
end
Railsのジェネレータで生成するファイルを制限しています。
ジェネレータを使用してmodel
、controller
のファイルを生成すると、テスト用のファイルも生成します。
以上でRSpecの設定は完了です。
given, when, thenのフレームワークとは
given、 when、 thenのフレームワークはテストをより構造化し、可読性を高め、テストケースの意図を明確にするために非常に役立つ記法です。 各セクションに分けてテストを記述することで、テストが何をしているのか、どのような条件で実行されるのか、期待される結果は何か、が明確になります。 これにより、テストケースの理解やメンテナンスが容易になります。
given
- テストを実行するための前提条件を定義します。
- データを作成します。
when
- テストケースの条件に応じて、givenで定義したデータを変化させます。
- 変化させたデータを用いて、テスト対象のメソッドを呼び出し、テストを実行します。
then
- メソッドの呼び出しの結果が、期待される結果と一致するかを検証します。
Postモデルの単体テスト
DBスキーマの定義
options = "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci ROW_FORMAT=COMPRESSED"
create_table "posts", force: :cascade, options: options do |t|
t.string "title" # 投稿タイトル
t.text "content" # 投稿内容
t.datetime "created_at", null: false # 投稿日時
t.datetime "updated_at", null: false # 更新日時
end
db/SchemafileにPostテーブルを追加します。
マイグレーション
docker-compose exec web rake ridgepole:apply RAILS_ENV=test
Schemafileの内容をテスト環境にマイグレーションします。
Postモデルの作成
docker-compose exec web rails g model Post --skip-migration
Postモデルを作成します。
以下のファイルが作成されます。
invoke active_record
create app/models/post.rb # modelファイル
invoke rspec
create spec/models/post_spec.rb # テスト用のmodelファイル
invoke factory_bot
create spec/factories/posts.rb # テスト用オブジェクトデータ
Postモデルの定義
class Post < ApplicationRecord
# 空文字、11文字以上の場合はバリデーションエラー
validates :title, presence: true, length: { maximum: 11 }
# 空文字、100文字以上の場合はバリデーションエラー
validates :content, presence: true, length: { maximum: 100 }
end
titleとcontentのバリデーションを追加します。
テストデータを作成
FactoryBot.define do
factory :post do
# title "Cooking"
title { Faker::Hobby.activity }
# 英数字のランダムな文字列を生成する(100文字)
content { Faker::Lorem.characters(number: 100) }
created_at { DateTime.now }
updated_at { DateTime.now }
end
end
Postモデルのテストを行うため、Fakerを使用してテストデータを作成します。
Fakerの使い方はFaker::[ジャンル].[タイトル]
と入力すればダミーデータが生成できます。
どんなものが用意されているかは公式から確認してください。
https://github.com/thoughtbot/factory_bot
テストの作成
今回のテストは以下のテストケースで行います。
-
title、contentのバリデーションが通ること。
-
titleが空の場合はバリデーションが通るかどうか。
-
titleが10文字を超える場合はバリデーションが通るかどうか。
-
contentが空の場合はバリデーションが通るかどうか。
-
contentが100文字を超える場合はバリデーションが通るかどうか。
require 'rails_helper'
RSpec.describe Post, type: :model do
# given データの準備
let(:post) { build(:post) } # モデルのテストデータを準備
describe "title、contentのバリデーション" do # テストの対象が何かを記述する。
it "title、contentのバリデーションが通ること" do # 期待値を記述する
# when 変化
# then 結果を比較
expect(post).to be_valid
end
end
describe "titleのバリデーション" do # テストの対象が何かを記述する。
it "titleが空の場合はバリデーションする" do # 期待値を記述する
# when 変化
post.title = ''
# then 結果を比較
expect(post).to_not be_valid
end
it "titleが10文字を超える場合はバリデーションする" do
# when 変化
post.title = 'a' * 12
# then 結果を比較
expect(post).to_not be_valid
end
end
describe "contentのバリデーション" do # テストの対象が何かを記述する。
it "contentが空の場合はバリデーションする" do # 期待値を記述する
# when 変化
post.content = ''
# then 結果を比較
expect(post).to_not be_valid
end
it "contentが100文字を超える場合はバリデーションする" do
# when 変化
post.content = 'a' * 101
# then 結果を比較
expect(post).to_not be_valid
end
end
end
-
let(:post) : letは遅延評価と呼ばれ、定義時の処理は実行されません。expect(post)のpostが呼ばれたタイミングでPostモデルのインスタンスを生成します。
-
build(:post) : メモリに一時的にインスタンスを生成します。
-
expect(post).to : メソッドの呼び出しの結果が、期待される結果と一致するかを検証します。期待される結果と一致する場合はテストが成功し、不一致な場合はテストが失敗します。
-
expect(post).to_not : メソッドの呼び出しの結果が、期待される結果と一致するかを検証します。期待される結果と不一致な場合はテストが成功し、一致する場合はテストが失敗します。
-
be_valid : バリデーションが通るかどうかを試しています。
-
メソッドの呼び出しの結果が、期待される結果と一致するかを検証します。
テストの実行
docker-compose exec web rspec spec/models/post_spec.rb
テストの結果
テストが成功した場合は、成功した各タイトルが緑色
で表示され、テストが失敗した場合は、失敗した各タイトルが赤色
で表示されます。この場合は各タイトルが緑色に表示され、5つのテストが正常に実行されたことを示しています。
テスト結果の内容
-
title、contentのバリデーションが通ること。
expect(post).to
の呼び出しの結果post
が、期待される結果be_valid
と一致したため、テストは成功しました。 -
titleが空の場合はバリデーションが通るかどうか。
expect(post).to_not
の呼び出しの結果post
が、期待される結果be_valid
と不一致だったため、テストは成功しました。 -
titleが11文字を超える場合はバリデーションが通るかどうか。
expect(post).to_not
の呼び出しの結果post
が、期待される結果be_valid
と不一致だったため、テストは成功しました。 -
contentが空の場合はバリデーションが通るかどうか。
expect(post).to_not
の呼び出しの結果post
が、期待される結果be_valid
と不一致だったため、テストは成功しました。 -
contentが100文字を超える場合はバリデーションが通るかどうか。
expect(post).to_not
の呼び出しの結果post
が、期待される結果be_valid
と不一致だったため、テストは成功しました。
まとめ
RSpecはグラフィカルなテスト結果表示機能により、テストコードを書くことが苦手な方でも、テスト結果を簡単に把握できます。また、RSpecはテストコードを記述するための豊富な機能を提供しており、テストコードの記述を理解しつつ、より簡単に記述することができます。
テストコードを書くことが苦手な方もこれを機会に導入してみてはいかがでしょうか?