RSpec学習メモ(everyday Rails RSpecによるRailsテスト入門 第8章)

<スペックをDRYに保つ>

ここまで学んできた知識を使ってテストスイートを作成しても、コードはたくさん重複しており、DRY原則を破っている状態となる。

 

---------------------------------------------------------------------------------------

 

<サポートモジュール>

サポートモジュールを使用することで、重複するコードをspec/support ディレクトリ以下のモジュールファイルに切り分けることができる。

 

<spec/support/login_support.rb>

 

# ログイン処理をモジュールに切り分けて定義している。

 

module LoginSupport

  Def sign_in_as(user)

    Visit root_path

    click_link "Sign in"

    fill_in "Email", with: user.email

    fill_in "Password", with: user.password

    click_button "Log in"
  end

end

 

# RSpecの設定。ここでは RSpec.configure を使って上記のモジュールをincludeしている。

# 必ずしも必要ではなく、テスト毎に明示的にサポートモジュールをincludeする方法もある。

 

RSpec.configure do |config|

  config.include LoginSupport

end

 

<spec/features/projects_spec.rb>

require 'rails_helper'

 

RSpec.feature "Projects", type: :feature do

    include LoginSupport

    # 明示的にサポートモジュールをincludeしている。

 

    # ユーザーは新しいプロジェクトを作成する

    scenario "user creates a new project" do

      # ...

    end

end

 

モジュール内のメソッド名は、コードを読んだ時に目的パッとわかるような名前にすること。

 

---------------------------------------------------------------------------------------

 

<let で遅延読み込みする>

Beforeブロックを使用することでdescribeやcontext内部で、各テストの実行前に共通のインスタンス変数をセットアップできる。

しかし、それらはdescribeやcontextの内部に書いたテストを実行するたびに毎回実行される為、テストを遅くする可能性がある。

こうした問題に対処するために、RSpecはletというメソッドを提供している。letは呼ばれた時に初めてデータを読み込む、遅延読み込みを実現するメソッドである。

 

<spec/models/task_spec.rb>

require 'rails_helper'

 

RSpec.describe Task, type: :model do

    let(:project) { FactoryBot.create(:project) }

 

    # プロジェクトと名前があれば有効な状態であること

    it "is valid with a project and name" do

      task = Task.new(

          project: project,

          name: "Test task",

      )

      expect(task).to be_valid

    end

 

    # プロジェクトがなければ無効な状態であること

    it "is invalid without a project" do

      task = Task.new(project: nil)
      task.valid?
      expect(task.errors[:project]).to include("must exist")

    end

 

  # 名前がなければ無効な状態であること

    it "is invalid without a name" do

      task = Task.new(name: nil)
      task.valid?
      expect(task.errors[:name]).to include("can't be blank")

    end

end

 

 

4行目にあるletを使って必要となるプロジェクトを作成している。最初のテストではprojectが必要なため、letによってprojectが作成される。

それ以降のテストではprojectが必要ないのでprojectが作成されることはない。一つ目のテストの実行が終わると、projectは取り除かれる。

 

ポイント

Beforeブロックではテストデータをセットアップする時、インスタンス変数に格納していたが、letは必要に応じてデータを作成する。

 

---------------------------------------------------------------------------------------

 

<shared_context (contextの共有)>

letを使用することで、複数のテストで必要な共通のデータを簡単にセットアップすることができる。

一方、shared_contextを使用すると複数のテストファイルで必要なセットアップを行うことができる。

 

ポイント

①Beforeブロックで記載しているものをletを使用してリファクタリングする。

②別のファイルにletの部分を切り分ける。(以下例)

③コントローラースペックのletの部分を、include_context "project setup" に置き換える。

 

<spec/support/contexts/project_setup.rb>

RSpec.shared_context "project setup" do
  let(:user) { FactoryBot.create(:user) }
  let(:project) { FactoryBot.create(:project, owner: user) }

  let(:task) { project.tasks.create!(name: "Test task") }

end