はじめに
当記事は以下のような方の参考になるかと思います。
- Ruby on Rails初学者
- テストについて学びたい方
- テストについて一次情報を確認したが、あまり理解できなかった方
- RSpecの基礎について学びたい方
- RSpecを簡潔に書きたい方
執筆の理由
私はプログラミングの学習にスクールを利用しましたが、テストは他の項目と比較して理解が進んでいない方が多いような印象でした。(私を含め)
その為、自身の理解が進んだ際には、記事を投稿する事でアウトプットでより理解を深めると共に、初学者の方々の理解の助けになれればと考えていました。
これが記事投稿の理由です。
ちなみに、自身がテストについて理解が進んでいなかった理由としては、以下の3点が挙げられます。
- テストを軽視していた。
- カリキュラム全体を確認した結果、進捗に直接の影響を与えないと考えた。
- その性質上、DRY(Don't Repeat Yourself「何度も同じ事を記述せず、効率的にコーディングする」の意)にすることがやや制限される為、面倒に感じた。
数ヶ月前の理解が進んでいない状態の私自身に向けて、用語解説も含めて書いていきますので、おそらく大半の方が基礎中の基礎を理解出来るようになるかと思います。
開発環境と前提
Ruby : 2.5.1
Rails : 5.2.3
なお、今回はユーザー登録機能のあるアプリケーションを想定し、Userモデルに対してのテストを行っていきます。
gem 'devise' を使用しています。
手順
大きく分けて以下の4手順です。
1.gemのインストール
2.各ファイルの作成
3.テストコードの記載
4.テストを行う
gem 'rspec-rails' のインストール 〜 ターミナル操作
gem 'rspec-rails' のインストール
まずはRails内でRSpecを使用する為に、gem 'rspec-rails' をインストールします。
Gemfileに記載する際は場所に注意してください。
テスト用のgemなので、group :development, test do 〜 end内に記載します。
#省略
#記載する箇所に注意
group :development, :test do
gem 'rspec-rails'
end
#省略
上記のように記載できたら、
ターミナル(著者はmacを使用しています。windowsでしたらコマンドプロンプトでしょうか?)操作を行います。
ターミナル操作
「bundle install」を実行。
$ bundle install
インストールできたら、
「rails g rspec:install」を実行してください。
$ rails g rspec:install
上記のコマンドで、rspec関連のファイルが自動作成されます。
上手くいけば、おそらく以下のような結果がターミナルに表示されるかと思います。
create .rspec
create spec
create spec/spec_helper.rb
create spec/rails_helper.rb
これで 'rspec-rails'の導入は完了しました。
テストコードを書くファイルの作成
テストコードは先ほど自動作成された「spec」内に新たにディレクトリ(≒フォルダ)、ファイルを作成し、そこに記載していきます。
今回は、Userモデルに記載のバリデーションをテストする為、spec直下に「models」というディレクトリを作成し、さらにその直下にuser_spec.rbというファイルを作成します。
(他にもモデルテストを行いたい場合は、modelsディレクトリ直下に「モデル名_spec.rb」というファイルを用意します。以下参照)
#例です。今回こちらのファイルは使用しません。
今回テストするバリデーションの確認
前提としてdeviseを導入済みなので、わざわざ記載しなくてもバリデーションがかかっている箇所もありますが、今回は理解の助けになるようあえて記載します。
自身のアプリの使用上「name」が「nickname」になっていたりしますが、特に結果に影響を与えないので、気にしないで下さい。
class User < ApplicationRecord
#省略
validates :nickname, presence: true, length: { minimum: 4 }
#省略
end
今回は、
nicknameに
- presence: true・・・空欄は認めない
- length: { minimum: 4 }・・・最小文字数4文字
以上のバリデーションをかけてみます。
テストコードの記載
例として、先程spec配下に作成したファイルに**'nicknameが空欄の場合登録できない'**テストコードを書いてみます。
require 'rails_helper' #require 〜 ・・・rubyのメソッド。外部ファイルの読み込みを行う。
describe User do
describe '#create' do
it 'nickname空欄の場合登録できない' do
user = User.new(
nickname: "",
email: "aaa@gmail.com",
password: "000000",
password_confirmation: "000000"
)
user.valid?
expect(user.errors[:nickname]).to include("can't be blank", "is too short (minimum is 4 characters)")
end
end
end
最上部に「require 'rails_helper'」とありますが、これは「rails g rspec:install」をした際に自動作成された**「rails_helper.rb」を読み込みます**、という意味です。
あらかじめrails_helper.rbに各ファイル共通の設定を記載しておき、テストを実行する際には「require 'rails_helper'」によってその共通設定を読み込むことで、共通の設定を使用します。
rails_helper.rb内のデフォルトの記載に関してのここでの解説は、割愛させていただきます。
以下、各行の説明です。
-
describe User do
descrive 〜 do内には何を記載しても構いません。が、コードを読む人がわかりやすいように書く必要があります。
この場合Userモデルについての内容の為、「User」と記載してあります。
「これからUserモデルについて書きますよ」という宣言のようなものです。 -
descrive '#create' do
同じくdescrive 〜 do内に何を記載しても構いません。
この場合、直上のコードと合わせて「これから(Userを)createする際の事を書きますよ」といった宣言をする内容になっています。
ちなみに、createの前に**#**がついていますが、これはメソッド名を記載する際にはつけるという慣習に倣って記載しています。
記載者以外がコードを読む際の可読性を向上させる為のものだと理解しています。 -
it 'nicknameが空欄の場合登録できない' do
it 〜 do内にテストの内容をわかりやすく記入します。
(私なりにわかりやすく記載したつもりですが、実務で行う際は各企業のやり方に従ってください。個人開発レベルでは問題ないかと思われます。)
-
user = User.new(〜)
この行ではテストにかける新規のインスタンスを作成しています。
User.new(〜)でカッコ内の情報を持つインスタンスを作成し、左辺のuserに代入しています。
今回は「nicknameが空欄の場合 登録できない 」事を確かめたいので、カッコ内のnicknameを "" として、あえて空欄にしています。
このnicknameが空欄のuserをテストにかけていきます。 -
user.valid?
valid?で先程作成したuserがバリデーションにかかるかどうか(実際に保存できる内容かどうか)を判定します。
バリデーションの内容は、
presence: true、つまり「空欄は認めない」というものですので、バリデーションにかかることが予想されます。 -
expect(user.errors[:nickname]).to include("can't be blank", "is too short (minimum is 4 characters)")
そして、その予想を書くのがここです。エクスペクテーションといいます。
構文としては、**expect(X).to マッチャ(Y)です。(マッチャは後述で説明します)
昔、英語の学習をする際にexpect 人 to do〜「人が〜するのを期待する/人に〜するよう要望する」**というのを習った気がしますが、それに非常に似た形で作られているかと思います。というかそのままでしょうか。
今回の場合は、user(先程作成した新規インスタンス)のnicknameカラムに関してのエラーメッセージにcan't be blankとis too short (minimum is 4 characters)が含まれるというエクスペクテーションになっています。
細かく見ていくと、
- user.errors[:nickname]
まずuserは先程作成したuserです。
errorsはメソッドで、ここでは先程の**user.valid?**でfalseの場合の「理由=エラーメッセージ」を作成します。
よくわからない場合は、ターミナルでコンソールを立ち上げると理解が深まるかと思います。
---コンソール立ち上げ---
$ rails c
Running via Spring preloader in process 46362
Loading development environment (Rails 5.2.3)
---インスタンス生成&userに代入---
[1] pry(main)> user = User.new(nickname: "", email: "aaa@gmail.com", password: "000000", password_confirmation: "000000")
(0.8ms) SET NAMES utf8, @@SESSION.sql_mode = CONCAT(CONCAT(@@sql_mode, ',STRICT_ALL_TABLES'), ',NO_AUTO_VALUE_ON_ZERO'), @@SESSION.sql_auto_is_null = 0, @@SESSION.wait_timeout = 2147483
=> #<User id: nil, nickname: "", email: "aaa@gmail.com", workplace: nil, created_at: nil, updated_at: nil, image: nil>
---user.valid?の実行---
[2] pry(main)> user.valid?
User Exists (84.4ms) SELECT 1 AS one FROM `users` WHERE `users`.`email` = BINARY 'aaa@gmail.com' LIMIT 1
User Exists (0.6ms) SELECT 1 AS one FROM `users` WHERE `users`.`email` = BINARY 'aaa@gmail.com' LIMIT 1
=> false
---直上の行でfalseが出ている。作成したuserがバリデーションにかかっていることがわかる。---
---user.errorsの実行---
[3] pry(main)> user.errors
=> #<ActiveModel::Errors:0x00007fc2da5852d8
@base=#<User id: nil, nickname: "", email: "aaa@gmail.com", workplace: nil, created_at: nil, updated_at: nil, image: nil>,
@details={:nickname=>[{:error=>:blank}, {:error=>:too_short, :count=>4}]},
@messages={:nickname=>["can't be blank", "is too short (minimum is 4 characters)"]}>
最後の行を見てみると、
messages={:nickname=>["can't be blank", "is too short (minimum is 4 characters)"]}
とありますが、これがバリデーションにかかった理由、エラーメッセージです。。
nicknameに関して、
- "can't be blank"・・・「空欄にできない」
- "is too short (minimum is 4 characters)"・・・「最小文字数4文字」
とあります。
テストコードで書いた通りの内容になっているので、このテストは通過するはずですね。
- .to〜
「〜であること(期待する)」という意味になるメソッドです。ただ、私が確認した限り構文内の**expect(X)の後に必ず付くものなので、取り急ぎは必ず付けるとしていた方が良いかもしれません。
ちなみに、「〜でないこと(を期待する)」場合には.not_to〜もしくは.to_not〜**を利用します。
- include
この部分をマッチャと言い、この後ろに記述する内容と**expect(X)のカッコ内の関連性について記述します。
includeもマッチャの一つで、「〜を含む」という内容です。
この他にもeq「等しい」やbe_valid「バリデーションにかからない」**などがありますが、こういった内容に関しては他記事に譲ることにします。
- ("can't be blank", "is too short (minimum is 4 characters)")
ターミナルで確認出来る、バリデーションにかかった際のメッセージです。
実際にテストにかけてみる
まずは以下のコードを$ rails g rspec:installをした際に作成された.rspecファイルに追記します。
--format documentation
これを記載することによって、テストにかけた際のメッセージが理解しやすいものになります。
--format documentation
次に、ターミナルでbundle exec rspecを実行します。
これが実際にテストにかける為のコマンドです。
$bundle exec rspec
すると、以下のように出力されるかと思います。
$bundle exec rspec
#省略
User
#create
nickname空欄の場合登録できない
Finished in 0.16438 seconds (files took 11.21 seconds to load) #この行は環境によって異なります。(かかった時間を表示)
1 example, 0 failures
最下部のexampleとfailerはそれぞれ「実行したテストの数」と「テストが通過しなかった数について」表示しています。
よって、今回はテストが通過したという結果になりました。
ちなみに、、、
テストが通過しない場合は以下のような出力になります。
(安直ですが、今回はエクスペクテーション内の**.toを.not_to**に書き換えて意図的に通過しないテストコードを書きました。)
#書き換え部分
expect(user.errors[:nickname]).not_to include("can't be blank", "is too short (minimum is 4 characters)")
$bundle exec rspec
#省略
User
#create
nickname空欄の場合登録できない (FAILED - 1)
Failures:
1) User#create nickname空欄の場合登録できない
Failure/Error: expect(user.errors[:nickname]).not_to include("can't be blank", "is too short (minimum is 4 characters)")
expected ["can't be blank", "is too short (minimum is 4 characters)"] not to include "can't be blank" and "is too short (minimum is 4 characters)"
# ./spec/models/user_spec.rb:11:in `block (3 levels) in <top (required)>'
Finished in 0.15455 seconds (files took 6.67 seconds to load)
1 example, 1 failure
Failed examples:
rspec ./spec/models/user_spec.rb:7 # User#create nickname空欄の場合登録できない
今回は1つしかテストにかけていませんが、先程と違い1 example, 1 failureと出力されています。
テストに通過していないということですね。
また、Faileresということで、テストにかけた際に通過しなかったエクスペクテーションが具体的にどのテストなのかが1),2)・・・と一覧表示されます。
この結果をもってテストコードの内容が間違っているのか、スペルミスがあるのか、テストコードが間違っていないのであれば、そもそもの仕様に問題があるのかという検証に入るのかと思われます。
編集後記
今回の内容は私が通っているスクールのカリキュラム内容に沿ったものになっているのですが、やや疑問を感じ自身で調べたところ、登録できない事を確かめるエクスペクテーションに
expect(user.save).to be_falseyというものが出てきました。
(userは当記事の内容に沿った表記としています)
つまり、nicknameが空のuserは保存(登録)できない。という内容です。
まさに今回書きたい内容ですし、より簡潔にかけるなぁと思ったんです。
これでいいのでは?とスクールのメンターに質問したところ、「今回は保存できないことが明示的で、後にコードを見返した時に保存できない理由がひと目でわかるように書く必要がある」との事でした。
実務ではそういった意識が必要であることを痛感しました。
最後に
factory_botというgemを利用する事でより簡潔にテストコードを書く方法があります。
その内容については、また別の記事で書いていこうと思います。
今回が初めての投稿なので不備があるかもしれませんが、ご容赦ください。
出来る限り「備忘録」的な内容でなく、他の初学者の方が読んだ際の参考になるよう書いてきました。
どこかの誰かのお役に立てることを切に願います。