RSpecとの距離を縮めるシンプルな書き方
IVRyで、バックエンドエンジニアをしている島筒です。
IVRyでは、Railsを利用して一部のサービスを提供しており、テストは、RSpecを利用しています。
10年近くRailsを使った開発に携わっていますが、Railsを始めて数年は、RSpecを書くことがとても億劫でした。
それが今では、RSpecなしでは継続的な開発は難しいという意識にまでなったのだから、自分でも不思議に思います。
苦手意識が変わったきっかけは、既存関数を修正した際、その関数を利用しているコードを破壊するような修正をRSpecのおかげで気づけたことです。
こういった経験を体験すると、RSpecなしの開発できなくなります。
安心して改善・リファクタリングが行えます。
今回の記事では、何がきっかけで、RSpecの苦手意識が払拭できたか、記しておこうと思います。
- 最初に定義するテストは、一番シンプルな正常系を確認する
- modelのテストから書いていく
- describe, context, itを書く
- rubocopを入れて、1つの関数のコード量をできる限り減らし、シンプルにする
- matcherは覚えなくて良い
※ これらは、誰もが同じように思えるというわけではないですし、当たり前ことばかりかもしれませんが、私にとってきっかけになった事柄です。
RSpecについて書くなんて、今更?とか言わないで😅
1. 最初に定義するテストは、一番シンプルな正常系を確認する
一番シンプルなテストコードは何かといえば、正常系の処理です。
正常系を定義した後、異常系のテストコードを追加していきます。
早速、コードを紹介します。
あるUserモデルがあり、emailとnameが必須とした時、以下のようなクラスが定義されると思います。
class User < ApplicationRecord
validates :email, presence: true
validates :name, presence: true
end
このクラスに対応した正常系のテストコードは、以下のようになります。
RSpec.describe User, type: :model do
describe '.valid' do
let(:user) { build(:user, email: 'テスト', name: 'test@example.com') } # FactoryBotを利用しています。
it { expect(user.valid).to be_truthy }
end
end
上記のように正常系のコードが定義されていれば、後は異常系の条件を追加していくだけになります。
RSpec.describe User, type: :model do
describe '.valid' do
let(:user) { build(:user, email: 'test@example.com', name: 'テスト') } # FactoryBotを利用しています。
it { expect(user.valid).to be_truthy }
context 'when email is empty' do
let(:user) { build(:user, email: '', name: 'テスト') }
it { expect(user.valid).to be_falsey }
end
context 'when name is empty' do
let(:user) { build(:user, email: 'test@example.com', name: '') }
it { expect(user.valid).to be_falsey }
end
end
end
これに気づいたことが、テストを書くハードルを一番下げてくれました。
2. modelのテストから書いていく
modelのテストから書くことで、関連したテスト、特にRequestスペックを書くハードルが下がりました。
前述のモデルのテストで書いた let(:user) { build(:user, email: 'test@example.com', name: 'テスト') }
を利用して、users_controllerのRequestスペックを書いてみます。
RSpec.describe UsersController, type: :request do
describe 'GET /users' do
before do
create(:user, email: 'test@example.com', name: 'テスト')
get users_path
end
it { expect(response).to have_http_status(:ok) }
end
end
create(:user, email: 'test@example.com', name: 'テスト')
の部分ですが、FactoryBotに登録しておくことで、もっと楽に書けるようになります。
3. describe, context, itを書く
describe: テストの対象
context: 条件や状態
it / example: テスト
当たり前のことなんですが、describe, context, itを正しく使いましょう。
正しく使うことで、テスト対象と条件が明確になり、コードの見通しが良くなります。
コードはたびたび改善されていくものです。
コード変更時、テストコードの見通しが悪いとテストコードを追随していくことが辛くなり、放置されることもあります。
テストコードの見通しが良いことで、テストを書くことが辛くなくなります。
書くときのコツを以下のテストコードを例に説明します。
RSpec.describe User, type: :model do
let(:user) { build(:user, email: 'test@example.com', name: 'テスト') }
it { expect(user.valid).to be_truthy }
context 'when email is empty' do
let(:user) { build(:user, email: '', name: 'テスト') }
it { expect(user.valid).to be_falsey }
end
end
RSpecは、文章として読むことができるようなっています。
1つ目のitは、「Userモデルの、user.valid
の結果は、trueであることを期待します。」を意味しています
RSpec.describe User, type: :model do
→ 「Userモデル」についてのテストであること宣言
it { expect(user.valid).to be_truthy }
→ user.valid
の結果は、trueであることを期待します。
2つ目のitは、「Userモデルの、emailがemptyの時、user.valid
の結果は、falseであることを期待します。」を意味しています
RSpec.describe User, type: :model do
→ 「Userモデル」についてのテストであること宣言
context 'when email is empty' do
→ 「emailがemptyの時」という状態を表す
it { expect(user.valid).to be_truthy }
→ user.valid
の結果は、falseであることを期待します。
4. rubocopを入れて、1つの関数のコード量をできる限り減らし、シンプルにする
RSpecのために導入したわけではありませんでしたが、結果的にRSpecを書くときに役立ちました。
以下は、ツッコミどころ満載のコードですが、以下のようなコードをrubocopは許しません。
こういったネストの深い関数、コードが連なっているコードに対し、RSpecを書こうと思うと躊躇してしまいます。
def save_and_save_file_and_send_mail(upload_file)
ActiveRecord::Base.transaction do
if save
if upload_file.present?
extension = upload_file.extname
File.open(Rails.root.join("public/myprofile/#{id}#{extension}"), 'w+') do |f|
f.write(upload_file.read)
end
end
if email.present?
UserMailer.send_mail(email, 'ユーザ情報を更新しました', self)
end
else
return false
end
end
end
以下のように書き換えると
def save_and_save_file_and_send_mail(upload_file)
ActiveRecord::Base.transaction do
return false unless save
write_myprofile(upload_file)
send_mail
end
end
def write_myprofile(upload_file)
return if upload_file.present?
extension = upload_file.extname
File.open(Rails.root.join("public/myprofile/#{id}#{extension}"), 'w+') do |f|
f.write(upload_file.read)
end
end
def send_mail
return if email.present?
UserMailer.send_mail(email, 'ユーザ情報を更新しました', self)
end
コードも見やすくなりましたが、RSpecも容易になりました。
RSpec.describe User, type: :model do
describe '.save_and_save_file_and_send_mail' do
let(:user) { build(:user, email: 'test@example.com', name: 'テスト') }
it { expect(user.save_and_save_file_and_send_mail).to be_truthy }
end
describe '.write_myprofile' do
let(:user) { build(:user, email: 'test@example.com', name: 'テスト') }
let(:upload_file) { Rack::Test::UploadedFile.new(File.join(Rails.root, 'spec/fixtures/profile.png'), 'image/png') }
before do
user.write_myprofile(upload_file)
end
it { expect(Rails.root.join("public/myprofile/#{user.id}#.png")).to be_exist }
end
describe '.send_mail' do
let(:user) { build(:user, email: 'test@example.com', name: 'テスト') }
it { expect { user.send_mail }.to change(ActionMailer::Base.deliveries, :size).by(1) }
context 'when email is empty' do
let(:user) { build(:user, email: 'test@example.com', name: 'テスト') }
it { expect { user.send_mail }.to not_change(ActionMailer::Base.deliveries, :size).by(1) }
end
end
end
一つの関数の中で条件分岐し、複雑だったコードがシンプルになりました。
5. matcherは覚えなくて良い
RSpecのmatcherは、数多く存在します。
初期は、matcherって何のメリットがあるの?と思うことが多々あります。
ちょっと困った時、助けてくれるのがmatcherです。
matcherを一つ一つは覚えなくても、必要になったとき、困ったとき、matcherを調べて利用できるか調べてみるだけで十分です。
- 配列を比較するとき、関数が並び順を担保していないため、たまに失敗する時
- 1行のコードを短くしたい時
- 例外の発生を確認したい時
良いRSpecライフを!
最後に
IVRyでは一緒に働いてくれるエンジニアを募集中です!
Discussion