🥰

RSpecずの距離を瞮めるシンプルな曞き方

に公開

IVRyで、バック゚ンド゚ンゞニアをしおいる島筒です。
https://twitter.com/kshimadutsu

IVRyでは、Railsを利甚しお䞀郚のサヌビスを提䟛しおおり、テストは、RSpecを利甚しおいたす。
10幎近くRailsを䜿った開発に携わっおいたすが、Railsを始めお数幎は、RSpecを曞くこずがずおも億劫でした。
それが今では、RSpecなしでは継続的な開発は難しいずいう意識にたでなったのだから、自分でも䞍思議に思いたす。

苊手意識が倉わったきっかけは、既存関数を修正した際、その関数を利甚しおいるコヌドを砎壊するような修正をRSpecのおかげで気づけたこずです。
こういった経隓を䜓隓するず、RSpecなしの開発できなくなりたす。
安心しお改善・リファクタリングが行えたす。

今回の蚘事では、䜕がきっかけで、RSpecの苊手意識が払拭できたか、蚘しおおこうず思いたす。

  1. 最初に定矩するテストは、䞀番シンプルな正垞系を確認する
  2. modelのテストから曞いおいく
  3. describe, context, itを曞く
  4. rubocopを入れお、1぀の関数のコヌド量をできる限り枛らし、シンプルにする
  5. 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を調べお利甚できるか調べおみるだけで十分です。

  • 配列を比范するずき、関数が䞊び順を担保しおいないため、たたに倱敗する時
  • 行のコヌドを短くしたい時
  • 䟋倖の発生を確認したい時

良いRSpecラむフを

最埌に

IVRyでは䞀緒に働いおくれる゚ンゞニアを募集䞭です
https://ivry-jp.notion.site/IVRy-e1d47e4a79ba4f9d8a891fc938e02271

IVRyテックブログ

Discussion