今回はRspecについてまとめようと思います。
RSpecは、Rubyで書かれたアプリケーションの挙動・機能をテストするために利用されます。(テストフレームワーク)
導入
まずはRspecを使えるようにします。
group :test do gem "rspec-rails" end
Gemfileにrspec-rails
というGemを追加して、bundle installを行います。
Gemをインストールしたら、rails generate rspec:install
コマンドでセットアップを行います。
成功すれば、specフォルダが作成されているはずなので、確認しましょう。
rails generate rspec:installを実行する前に、config/application.rb
に下記設定を追加することで、specフォルダ内にデフォルトで作成されるファイルを作成されないように設定できます。
config.generators do |g| g.test_framework :rspec, fixtures: false, # テストデータを作るfixtureを作成しない view_specs: false, # テストデータを作るfixtureを作成しない helper_specs: false, # ヘルパー用のスペックを作成しない routing_specs: false # ルーティングのスペックを作成しない end
これで、RSpecを実行する準備が整いました。
テストの配置ルールと命名規則
テストを実行するためにはファイルの配置ルールと命名規則に従う必要があります。
Ruby on Railsの場合は、次のように配置します。
spec/ models/ モデルファイル名_spec.rb controllers/ コントローラファイル名_spec.rb views/ erbファイル名_spec.rb
このように、Ruby on Railsのディレクトリ構成と同じ階層構成でspecディレクトリ配下に配置します。
Rspecの基本構造
Rspecに限らず、基本的なテストはデータを準備→確認したい処理を呼び出す→結果が想定通りか確認という流れになると思います。
その流れの中で、Rspecの書き方を見ていきます。
まずは、ファイルの先頭にはrequire 'rails_helper'をつけてください!
describe
まず、describe
でテストの対象が何かを記述します。
describe "計算を確認するテスト" do end
このように日本語でも書くことができます(もちろん、英語の可能です)
また、どのクラスのテストケースかを指定するだけでなく、 type: :model
や type: :controller
を指定することで、どのモジュールのテストかを指定できます。
controllerのテストで type: :request
を指定できます。type: :requestはURLの接続テストになるので、APIのテストという意味合いをもちます。(エンドポイントやPOST/GETなどのメソッドの種類を指定して実行します)
RSpec.describe Bookmark, type: :model do end
context
contextは特定の条件や実施する際の条件を記載します。
describe "計算を確認するテスト" do context "マイナスの2つの数字の足し算" do calculator = Calculator.new result = calculator.add(-1, -2) end end
contextはネストすることができるので、1つのdescribeの中に複数のcontextを記載できます。
describe "計算を確認するテスト" do context "2つの数字を加算" do context "2つのプラスの数字を加算" do end context "2つのマイナスの数字を加算" do end end context "2つの数字を減算" do end end
it
itはアウトプットの内容を記載します。
describe "計算を確認するテスト" do context "マイナスの2つの数字の足し算" do calculator = Calculator.new result = calculator.add(-1, -2) it "-3が出力される(マイナス通しの足し算で想定通りになる)" do expect(result).to eq(-3) end end end
expect
メソッドの引数にテスト対象コード(オブジェクト)を渡します。
expect(...).to につづけてマッチャーを書きます。
今回はeq
でresultと-3が一致するかを確認しています。
resultと-3が一致しない場合にはテストケースの実行で失敗します。
eq以外にtrueかfalseかをチェックするbe_valid
などマッチャーはいろんな種類があるので、公式ドキュメントを確認するといいでしょう。(最後の参照にリンク貼ってます)
before
before
は各テストケースの前に実行されるコードブロックです。
テストケースの前にデータの準備を行いたいパターンやいろんなテストケースで共通に行いたい処理を記載するなどができます。
describe "計算を確認するテスト" do before do # セットアップコード end it "マイナス通しの足し算" do # テストケースの実装 end it "0が含まれた足し算" do # テストケースの実装 end it "マイナスの数字とプラスの数字の足し算" do # テストケースの実装 end end
let
letは変数を定義する際に使用します。 describeかcontextの中でのみ使用可能です。
letとlet!の2種類あります。 letとlet!の違いは実行タイミングです。 letは利用時に実行され、let!は書かれた場所で実行されます。
letは遅延評価とも言われています。 { create(:post,user: user) }はテストが実行されるまで実際には評価されません。
let(:post) { create(:post,user: user) }
let!にするとテストが始まる前に{ create(:post,user: user) }が実行され、 その結果がlet!の後の:postにセットされます。
let!(:post) { create(:post,user: user) }
つまり、letはあるテストケース内で変数を複数回使用するが、その値が変更されない時に便利です。 そして、let!はあるテストケース内で変数の値が変更される可能性があり、テストケースごとに初期化が必要な時に便利です。
テストの実行
specフォルダに移動して、bundle exec rspec
コマンドで全テストケースを実行します。
bundle exec rspec ./spec/ファイル名 というコマンドで特定のファイルに絞ってテスト実行もできます。
データ作成
テストデータを作成するときにはRspec標準で使えるfixture
に代わり、テストデータの準備をサポートしてくれるFactorybot
を使用します。
fixtureを使用することもありますが、今回はFactorybotについて記載します。
Gem追加
Gemfileにfactory_bot_rails
というGemを追加して、bundle installを行います。
group :test do gem "rspec-rails" gem "factory_bot_rails" end
ファイル作成
rails g factory_bot:model モデル名
上記コマンドでfactoryファイルを作成します。test/factoriesに作成されます。
モデル作成
ファイル作成で作られたファイルがUserだと仮定して、Userのテストデータを作成します。
FactoryBot.define do factory :user do sequence :email do |n| "person#{n}@example.com" end password { "Password1" } password_confirmation { "Password1" } name { "test" } status { 0 } end
password { "Password1" }はpasswordが固定で"Password1"という値になっていることを表しています。
sequence :email do |n| "person#{n}@example.com" end
上記はnの部分が連番でデータが作成されます。
データを呼び出す
作成したテストデータを呼び出す場合には下記のようにcreate(:user)
もしくは、build(:user)
とすることで作成したデータを使用できます。
require "rails_helper" describe "xxxxx" do before do @user = create(:user) #ここで定義 end context "xxxxx" do xxxxx end end
createとbuildには下記違いがあります。
create
- DB上にインスタンスを永続化する。
- DB上にデータを作成する。
- DBにアクセスする処理のときは必須。(何かの処理の後、DBの値が変更されたのを確認する際は必要)
build
- メモリ上にインスタンスを確保する。
- DB上にはデータがないので、DBにアクセスする必要があるテストのときは使えない。
- DBにアクセスする必要がないテストの時には、インスタンスを確保する時にDBにアクセスする必要がないので処理が比較的軽くなる。
さらに、データを上書きすることもできます。
@user = create(:user, name: "test更新")
このようにすることで、nameがtest更新のユーザーデータを作成できます。
trait
trait
はパターンA、B 、C などデータをパターン分けして準備できる機能です。
FactoryBot.define do factory :user do sequence :email do |n| "person#{n}@example.com" end password { "Password1" } password_confirmation { "Password1" } name { "test" } status { 0 } trait :fixed_id do id { 100 } end end
上記は先ほどUserモデルにidが固定バージョンのデータを追加しました。
let(:user) { create(:user, :fixed_id) }
のようにfixed_idを追加することで指定したデータにできます。
こうすることで、毎回idが固定は困るけど、特定のケースではidを固定で使用したいなど使い分けができます。
複数データを作成する
複数データを作成する場合に、個数分createするのは骨が折れる作業です。
そこで、FactoryBotのcreate_listメソッドを使って複数個のデータを一気に作成できます。
users = create_list(:user, 5)
または、Userの生成時に、Taskも一緒に生成したい場合には下記のようにすることができます。
trait :with_tasks do after(:create) do |user| create_list(:task, 5, user:) end end
TransientとEvaluatorを使用した関連データの生成
trasient
はファクトリの生成時に動的なデータにする属性です。
transientをcallback関数内で参照したい場合、evaluator
を使います。
callback関数のブロック引数でevaluatorを宣言することで、transientの値を参照することができます。
FactoryBot.define do factory :user do end trait :with_tasks do transient do tasks_count { 5 } end after(:create) do |user, evaluator| # evaluatorを経由して、transientのtasks_countを参照している create_list(:knowledge, evaluator.tasks_count, user:) end end end
上記はUserのwith_tasksで、生成するtaskの数を指定したい場合の定義です。
画像のサンプルを用意
画像のサンプルを用意する場合にはspec/fixtures
配下に画像を置いてください。
画像のアップロードを行うテストはfixture_file_upload
メソッドを使用します。
例えば、下記はアップロードされた画像とみなすことができます。
let(:image1) { fixture_file_upload("spec/fixtures/image.png", "image/png") }
おまけ
type: :requestでheaderを指定するのは簡単ですが、type: :controllerの場合には少し工夫が必要です。
describe 'Sample' do let(:header) { { 'X-Requested-With': "XMLHttpRequest" } } let(:params) { { 'TEST': "SAMPLE" } } it 'set header' do post 'path/to/endpoint', params: params, headers: header expect(response.status).to eq 200 end end
type: :controllerの場合にはheaderを指定するだけで適用されますが、type: :controllerでは指定できません。
require 'rails_helper' RSpec.describe Api::LoginsController, type: :controller do let(:header) { { 'X-Requested-With': "XMLHttpRequest" } } describe 'logout' do it 'returns status 204' do request.headers.merge!(header) post :logout expect(response.status).to eq(204) end end end end
type: :controllerで使用する場合にはrequest.headersに追加したいヘッダー情報をマージしましょう。