
RSpecを用いた単体テストについての備忘録 @TECH CAMP #11
どうも、もう6月なことにビビってます、とだです。
昨日は統合テストについて学んだことを簡単にお披露目しましたので、(順番的には逆ですが)今日は単体テストについて学んだことを備忘録としてお披露目したいと思います。
RSpecの導入
私が今学んでいるWebアプリケーションの作成はRuby on Railsを用いています。Ruby on Railsにおいては、基本的にはモデルとコントローラのファイルに対してテストコードを作成するようです。その際にRSpecという独自の言語を利用します。RSpecは、Rubyを元に作成されたテストに特化した言語です。
ということで、「RSpec」を用いてテストコードを書いていきます。
RSpecを利用するためには、Gemfileに「gem rspec-rails」と記述し、ターミナルでbundle installした後、さらに
$ rails g rspec:install
#RSpec用設定ファイルの作成
を実行してRSpec用の設定ファイルを作成します。
create .rspec
create spec
create spec/spec_helper.rb
create spec/rails_helper.rb
上記のようにターミナルに表示されればOKです。
その後、作成された「.rspec」というファイルに
--format documentation
と追記したら準備完了です。
上記で作成されたファイルで「rails_helper.rb」と「spec_helper.rb」は以下の役割があります。
rails_helper.rb
RailsにおいてRSpecを利用する際に、共通の設定を書いておくファイルです。各テスト用ファイルでこちらのファイルを読み込むことで、共通の設定や、メソッドを適用します。
spec_helper.rb
rails_helper.rbと同じくRSpec用の共通の設定を書いておくファイルですが、こちらはRSpecをRails無しで利用する際に利用します。
そもそも単体テストとは?
単体テストは、ひとつのプログラムのまとまりに関して、それ単体が正常に動くか確かめるテストのことです。例えばRailsであれば、モデルクラスひとつ、コントローラークラスひとつにつきそれぞれテストコードを書きます。
今回は例として、モデルのバリデーションに関するテストコードを備忘録として残しておきます。
まず、テストコードのファイル(specファイル)は、
対応するクラス名_spec.rb
という名前になります。
テストコードの基本的な文法
例として「1+1が2になるか確認する」という簡単なテストコードを例に基本的な文法をまとめます。以下がテストコードです。
describe "hogefuga" do
it "1 + 1は2になること" do
expect(1 + 1).to eq 2
end
end
注目すべきポイントがいくつかあるので、それぞれ説明します。
describe
1行目のdescribeは、直後の do から end までのテストのまとまりを作ります。テストの対象がなんであるかを表しています。describeの後に続く""の中にはそのまとまりの説明を書きます。例えば、"#create"となっていればcreateアクション(リソースを新規作成して追加(保存)するアクション)についてのテストという説明になります。
また、連続して記述するなどして入れ子状に(ネスト)することができます。
require 'rails_helper'
describe User do
describe '#create' do
it "1 + 1は2になること" do
expect(1 + 1).to eq 2
end
end
end
上記の例では、「Userクラスにあるcreateメソッドをテストするまとまり」を意味します。
it / example
2行目のitはexampleと呼ばれる実際に動作するテストコードのまとまりを表します。そのままexampleと記述してもいいですし、itと記述しても良いみたいです。
使い分けとして
日本語で記述するときはexampleを使う。it "is 〜やit { should be 〜 }のような形で書きたい場合はitを使う。
(Qiita: RSpecの(describe/context/example/it)の使い分け より引用)
という基準もあるようですが、itで統一して""の中身を日本語で書いても良いようです。この it の後に続く""の中にはそのexampleの説明を書きます。つまりテストの期待するアウトプット、テストの結果どうなるべきかという内容が記述される場所です。
エクスペクテーション
実際に評価される式のことです。it do ~ endの間に書きます。上記の式ではexpect(1 + 1).to eq 2の部分がエクスペクテーションに当たります。
expect(X).to eq Y
エクスペクテーションの文法です。xの部分に入れた式の値がYの部分の値と等しければ、テストが成功します。eqの部分を、マッチャと言います。
expect(X).to マッチャ Y という形で覚えました。
マッチャ
エクスペクテーションの中で、テストが成功する条件を示します。例えばeqは「等しければ」という意味になります。他にもinclude(含んでいればという意味で、引数にとった値がexpectの引数である配列に含まれているかをチェックすることができるマッチャ)、valid(バリデーションされれば)、be_valid(expectの引数にしたインスタンスが全てのバリデーションをクリアする場合にパスするマッチャ)など複数のマッチャが存在します。
この他にもcontextという、特定の条件は何かを表すための記述を書いて、テストをグループ分けすることもできます。
テストコードを書く際の原則について
単体テストのテストコードを書くにあたって、守るべき原則が5つあります。
①各exampleで期待する値は1つ
②期待する結果をはっきりわかりやすく記述
③起きて欲しいことと起きてほしくないこと両方をテストする
④境界値をテストする
⑤可読性を考えつつ、適度にDRYにする
①各exampleで期待する値は1つ
テストコードにおいては、example(it "exampleの説明" do ~ end のまとまり)ひとつに必ずエクスペクテーション(expext(◯◯).to ~)をひとつ含めます。
2つ以上含めてしまうと、どちらのエクスペクテーションでエラーが出たのか判別できず、正確なテストができないためです。
②期待する結果をはっきりわかりやすく記述する
先ほども述べましたが、it "〜" doの"〜"の部分は、期待する結果を書いておく場所、テストの結果どうなるかを書いておく場所です。
明快な、わかりやすい書き方をすることで、自分で確認する時やチームメンバーとの共有、クライアントへの仕様説明が楽になり、誤解を生まずコミュニケーションミスも減らせます。
③起きて欲しいことと起きてほしくないこと両方をテストする
起きて欲しいことをチェックするのは当然として、起きてほしくない場合にどんな結果が起こるかも想定し、その予想通りになるか確かないといけないようです。なぜなら予期せぬ動作が残るのを防ぐためだからです。
④境界値をテストする
例えば6文字以上でバリデーションに引っかかる、という条件の場合は「5文字までは正常」と「6文字以上ならば異常」を確かめるようにします。この時の「6」が境界値にあたります。これも、予期せぬ動作を防ぐためです。
⑤可読性を考えつつ、適度にDRYにする
まずDRYとは「Don't Repeat Yourself」の略で、何度も同じことを記述せず効率的にコードを書こう、という原則を意味します。ドライなんて、命令口調のような冷たい言葉で書くのか?と思いましたが違いました。
大事なのはテストコードにおいては何よりもわかりやすさを優先するということです。その結果たとえDRYに添えなくなったとしても、わかりづらくなってテストの見落としが起きるよりはましだからです。
テストコードの記述を簡単にするために便利なGem
テストコードではインスタンスを作成して値をセットすることが多いです。これをすべてのexampleで行うと大変です。この作業を効率化してくれるfactory_botというGemがあります。これは簡単にダミーのインスタンスを作成することができるGemです。他のファイルで予め各クラスのインスタンスに定めるプロパティを設定しておき、specファイルからメソッドを利用してその通りのインスタンスを作成します。factory_botを利用すれば、テストコードを短い記述にすることができます。
factory_botをbundle installしたら、specディレクトリ直下に「factories」というディレクトリを作成します。その中に、作成したインスタンスの複数形のファイル名でRubyのファイルを作成します。例えば、users.rbのようなファイルを作ってその中に以下のような記述をしてテストデータを定義します。
FactoryBot.define do
factory :user do
nickname {"toda"}
email {"totodada@gmail.com"}
password {"12345678"}
password_confirmation {"12345678"}
end
end
factory_botを用いてインスタンスを生成する際には、基本的なbuildメソッドやcreateメソッドを使います。
buildメソッドでは引数にシンボル型で取ったクラス名のインスタンスを、factory_botの記述をもとに作成します。例えば前述のusers.rbが存在する場合、下記2つの変数userの値は同じ値になります。
#factory_botを利用しない場合
user = User.new(nickname: "toda", email: "totodada@gmail.com", password: "12345678", password_confirmation: "12345678")
#factory_botを利用する場合
user = FactoryBot.build(:user)
またcreateメソッドではbuildとほぼ同じ働きをしますが、createの場合はテスト用のDBに値が保存されます。注意すべき点として、1回のテストが実行され、終了する毎にテスト用のDBの保存された値がすべて消去(ロールバック)されます。
またfactory_botによってインスタンスを作成する際に、レシーバーであるクラスのFactoryBotという記述を省略することができます。さらに詳しい説明は以下のQiitaがわかりやすいです。
テストコードの例
今回はユーザー登録ができるWebアプリケーションにおいて「Userモデル」というものが存在するとして、そのUserモデルのバリデーションに関するテストコードを紹介します。
Userモデルには以下のような記述が書かれているとします。ユーザー登録などのユーザー管理機能は「devise」というgemを用いています。
class User < ApplicationRecord
# Include default devise modules. Others available are:
# :confirmable, :lockable, :timeoutable, :trackable and :omniauthable
devise :database_authenticatable, :registerable,
:recoverable, :rememberable, :validatable
# アソシエーション
has_many :hogehoges
has_many :fugafugas
# バリデーション
validates :nickname, presence: true, length: { maximum: 6 }
end
これを踏まえて、今回テストする内容は以下の10パターンです。
ユーザーの登録時に確認が必要なバリデーションのテスト
1. nicknameとemail、passwordとpassword_confirmationが存在すれば登録できること
2. nicknameがない場合は登録できないこと
3. emailがない場合は登録できないこと
4. passwordがない場合は登録できないこと
5. passwordが存在してもpassword_confirmationがない場合は登録できないこと
6. nicknameが7文字以上であれば登録できないこと
7. nicknameが6文字以下では登録できること
8. 重複したemailが存在する場合登録できないこと
9. passwordが6文字以上であれば登録できること
10. passwordが5文字以下であれば登録できないこと
これらをテストするコードは以下の通りとなります。
require 'rails_helper'
describe User do
describe '#create' do
# 1
it "nickname、email、passwordとpassword_confirmationが存在すれば登録できること" do
user = build(:user)
expect(user).to be_valid
end
# 2
it "nicknameがない場合は登録できないこと" do
user = build(:user, nickname: "")
user.valid?
expect(user.errors[:nickname]).to include("can't be blank")
end
# 3
it "emailがない場合は登録できないこと" do
user = build(:user, email: "")
user.valid?
expect(user.errors[:email]).to include("can't be blank")
end
# 4
it "passwordがない場合は登録できないこと" do
user = build(:user, password: "")
user.valid?
expect(user.errors[:password]).to include("can't be blank")
end
# 5
it "passwordが存在してもpassword_confirmationがない場合は登録できないこと" do
user = build(:user, password_confirmation: "")
user.valid?
expect(user.errors[:password_confirmation]).to include("doesn't match Password")
end
# 6
it "nicknameが7文字以上であれば登録できないこと" do
user = build(:user, nickname: "1234567")
user.valid?
expect(user.errors[:nickname]).to include("is too long (maximum is 6 characters)")
end
# 7
it "nicknameが6文字以下であれば登録できること" do
user = build(:user, nickname: "123456")
user.valid?
expect(user).to be_valid
end
# 8
it "重複したemailが存在する場合登録できないこと" do
user = create(:user)
another_user = build(:user)
another_user.valid?
expect(another_user.errors[:email]).to include("has already been taken")
end
# 9
it "passwordが6文字以上であれば登録できること" do
user = build(:user, password: "123456", password_confirmation: "123456")
user.valid?
expect(user).to be_valid
end
# 10
it "passwordが5文字以下である場合は登録できないこと" do
user = build(:user, password: "12345", password_confirmation: "12345")
user.valid?
expect(user.errors[:password]).to include("is too short (minimum is 6 characters)")
end
end
end
コード中に出てきたメソッドを補足します。
valid?メソッド
valid?メソッドを利用すると、ActiveRecord::Baseを継承しているクラスのインスタンスを保存する際に「バリデーションにより保存ができない状態であるか」を確かめることができます。
errorsメソッド
valid?メソッドの返り値はtrue/falseなのですが、valid?メソッドを利用したインスタンスに対してerrorsメソッドを利用すると、バリデーションにより保存ができない状態である場合、なぜできないのかを確認することができます。
テストコードが書けたら…
ターミナルにおいて以下のコマンドを実行するとテストができます。
$ bundle exec rspec
または以下のように特定のファイルを選択してテストすることもできます。
$ bundle exec rspec spec/models/user_spec.rb
テストが成功すると以下のような結果が出てきます。
User
#create
nickname、email、passwordとpassword_confirmationが存在すれば登録できること
nicknameがない場合は登録できないこと
emailがない場合は登録できないこと
passwordがない場合は登録できないこと
passwordが存在してもpassword_confirmationがない場合は登録できないこと
nicknameが7文字以上であれば登録できないこと
nicknameが6文字以下であれば登録できること
重複したemailが存在する場合登録できないこと
passwordが6文字以上であれば登録できること
passwordが5文字以下である場合は登録できないこと
Finished in 0.39517 seconds (files took 3.76 seconds to load)
10 examples, 0 failures
これでテストは成功です!
終わりに
自分なりにざっとまとめたつもりですが、まだまだ単体テストだけでも、こんな記述ができるよとか、FakerというGemがあるよとか、コントローラーのテストの記述はモデルのテストとはまた違うよなどなどたくさんあります。
ちょっと今日中にまとめることはできないので追々まとめたいと思います。それでもここまで長々とお付き合いいただきましてありがとうございました。