Rails5「中級」チュートリアル(3-3)投稿機能: テスト(翻訳)

概要

概要

原著者の許諾を得て翻訳・公開いたします。

Rails5中級チュートリアルはセットアップが短めで、RDBMSにはPostgreSQL、テストにはRSpecを用います。
原文が非常に長いので分割します。章ごとのリンクは順次追加します。

翻訳時にRuby 2.5.0とRails 5.1.4で動作を確認しています。

注意: Rails中級チュートリアルは、Ruby on Railsチュートリアル(https://railstutorial.jp/)(Railsチュートリアル)とは著者も対象読者も異なります。

目次

Rails5「中級」チュートリアル(3-3)投稿機能: テスト(翻訳)

現時点までのアプリにはそこそこ機能が追加されています。たとえわずかな機能であっても、アプリでテストを書いてすべてが正常に機能するための時間を使わなければならない段階に既になっています。アプリの機能が現在の20倍に増えたときのことを想像してみましょう。コードを変更するたびに、すべてが問題なく動作していることを自力でチェックするのは相当なフラストレーションです。これを避けて手動テストの時間を削減するために、テスト自動化を実装することにします。

テストを書き始める前に、ここでテストについて説明します。Railsテスティングガイドも読んでRailsのテスト手法について慣れておくとよいでしょう。

訳注: テストについてCode Climateの以下の記事もどうぞ。

Railsのテスティングピラミッド(翻訳)

テストに使うもの

フレームワーク: RSpec

私がRailsアプリのテストを書き始めた頃はデフォルトのminitestを使っていましたが、今はRSpecを使っています。私はminitestとRSpecに優劣の違いはないと考えています。どちらも優れたフレームワークであり、私がRSpecを試すことに決めたのは、RailsコミュニティでRSpecの人気が高いと聞いていたからです。今ではほとんどのテストでRSpecを使っています。

サンプルデータ: factory_bot

こちらについても、当初の私はデフォルトのRails wayであるfixtureでサンプルデータを追加してみました。そしてサンプルデータの選択は、テスティングフレームワークを選択するときとは違うことがわかってきました。テスティングフレームワークの選択は、おそらく個人の好みでよいと思いますが、サンプルデータについては違うと思います。fixtureは当初はよかったのですが、アプリが成長するにつれてサンプルデータの制御が難しくなりました。私の選択ミスだったようです。一方factoryは使った当初から何の心配もなく、実に快調です。アプリの規模が大きくても小さくても、サンプルデータを作る手間は変わりません。

訳注: 原文ではfactory_girlでしたが、現在はfactory_botと名前が変わっているので、本記事ではすべてfactory_girl_railsとコードをfactory_bot_rails向けに置き換えました。

受け入れテスト: Capybara

Capybaraはデフォルトでrack_testドライバを使いますが、残念ながらこのドライバはJavaScriptをサポートしていません。Capybaraのデフォルトドライバの代わりに、poltergeistを使うことにします。poltergeistはJavaScriptをサポートし、ドライバの設定が私にとって最も簡単でした。

訳注: Rails 5.1ではCapybaraが標準で使えるようになっているほか、「システムテスト」機能が導入されて、RSpec 3.7以降とChromedriverを使ってCapybaraの受け入れテスト(system spec)を楽に設定できるようになっています。今後はシステムテストがRailsの受け入れテストで主流になる可能性もあります。詳しくは以下のTechRacho記事をご覧ください。

Rails 5.1以降のシステムテストをRSpecで実行する(翻訳)

テストの対象

私は自分で書いたロジックをすべてテストしています。テスト対象には以下があります。

  • ヘルパー
  • モデル
  • ジョブ
  • デザインパターン
  • その他自分が書いたロジックすべて

ロジックの他に、Capybaraでアプリの受け入れテストを書いてユーザー操作をシミュレートし、アプリの機能がすべて正常に動作することを確認するようにしています。受け入れテスト(シミュレーションテスト)を補うために、request specを使ってすべてのリクエストが正しいレスポンスを返すことを確認します。

以上のテストは私のニーズに十分合うので、私の個人アプリではこれらをテストしています。当然ですが、テスト方法の標準は人それぞれ、会社ごとに異なることがあります。

コントローラ/ビュー/gemのテストが含まれていない理由がおわかりでしょうか。多くのRails開発者が指摘しているように、コントローラやビューにはロジックを含めるべきではありません。私もこれに賛成です。これらにはテストの必要な部分がそれほどありません。コントローラやビューのテストは、ユーザーシミュレーションテスト(受け入れテスト)で十分効果を得られると私は考えています。gemは既に開発者がテストしていますし、これもシミュレーションテストで十分カバーできると思います。

テスト方法

私ももちろん、可能な限りTDD(テスト駆動開発)アプローチを使うようにしています。TDDでは最初にテストを書き、それからコードを実装します。TDDでは開発フローがよりスムーズになりますが、最終的な機能がどんなふうに見えるか、何が出力されるかが事前にわからないこともあります。コードで実験する場合や、違う実装を試す場合は、テストを最初に書いてから実装する方法だとあまりうまくいきません。

私は何らかのロジックを書く前には必ず(前述のとおり、必ずしもそうではありませんが)、独立したテストすなわち単体テストを書きます。アプリの機能がすべて正常に動作することを確認するために、Capybaraで受け入れテスト(ユーザーシミュレーションテスト)を書きます。

テスト環境のセットアップ

最初のテストを書く前に、テストのための環境を設定しなければなりません。

Gemfileを開いて以下のgemをtestグループに追加します。

gem 'rspec-rails', '~> 3.6'
gem 'factory_bot_rails'
gem 'rails-controller-testing'
gem 'headless'
gem 'capybara'
gem 'poltergeist'
gem 'database_cleaner'

再度説明すると、rspec gemはテスティングフレームワーク、factory_botはサンプルデータ追加用、capybaraはアプリのユーザー操作のシミュレーション用、poltergeistはテストでJavaScriptをサポートするためのドライバです。

訳注: Rails 5.1以降ではCapybaraが標準で使えるのでGemfileでの追加は不要になります。

Rails 5.1以降のシステムテストをRSpecで実行する(翻訳)

JavaScriptをサポートする別のドライバを使ってセットアップすることもできます。poltergeist gemを使う場合はPhantomJSのインストールが別途必要です。インストール方法についてはpoltergeistのドキュメントをご覧ください。

headlessは、ヘッドレスドライバをサポートするのに必要です。poltergeistはヘッドレスドライバなのでこのgemが必要になります。rails-controller-testing gemはrequests specを使ってリクエストとレスポンスをテストするときに必要になります。詳しくは後述します。

訳注: poltergeistheadlessの代わりにchromedriver-helper gemを使うこともできます。インストール方法は以下の記事をご覧ください。

Rails 5.1以降のシステムテストをRSpecで実行する(翻訳)

database_cleaner gemは、テストでJavaScriptを実行した後にtestデータベースをクリーンアップするのに必要です。通常、testデータベースはテストのたびにクリーンアップされますが、JavaScriptを使う機能をテストする場合にデータベースが自動クリーンアップされないことがあります。この動作は今後変更されると思われますが、本チュートリアル執筆時点ではJavaScript実行後にtestデータベースは自動クリーンアップされません。そのため、JavaScriptの各テストの後にtestデータベースが自動クリーンアップされるようテスト環境を手動で構成しなければなりません。database_cleaner gemが実行されるタイミングについてはこの後設定します。

訳注: 最新のRails 5.1でシステムテストを行う場合、database_cleaner gemは不要になりました。

Rails 5.1以降のシステムテストをRSpecで実行する(翻訳)

gemの利用目的の説明が終わりましたので、以下を実行してgemをインストールしましょう。

bundle install

以下を実行して、RSpecで使うspecディレクトリを初期化します。

rails generate rspec:install

RSpecフレームワークでは、1つのテストを一般に「spec」と呼びます。specを実行するということは、テストを実行するということです。

プロジェクトディレクトリの下にspecというディレクトリができます。テストは今後ここに書きます。他にtestというディレクトリがありますが、これはデフォルトのテスト設定の場合にテストを保存する場所です。このディレクトリは今後使わないので、削除して構いませんc(x_X)b。

上述したように、JavaScriptを使うテスト向けにdatabase_cleanerのセットアップを行わなければなりません。rails_helper.rbファイルを開きます。

spec/rails_helper.rb
config.use_transactional_fixtures = true

上の行を以下に変更します。

config.use_transactional_fixtures = false

さらに以下のコードも追加します(Gist

# spec/rails_helper.rb
  config.before(:suite) do
    DatabaseCleaner.clean_with(:truncation)
  end

  config.before(:each) do
    DatabaseCleaner.strategy = :transaction
  end

  config.before(:each, :js => true) do
    DatabaseCleaner.strategy = :truncation
  end

  config.before(:each) do
    DatabaseCleaner.start
  end

  config.after(:each) do
    DatabaseCleaner.clean
  end

上のコードスニペットはこちらのチュートリアルのものを使いました。

最後に、もう少し設定を行います。rails_helper.rbファイルの設定に以下を追加します(Gist)。

訳注: require以外の設定は、RSpec.configure do |config|ブロックの中に書きます。

# spec/rails_helper.rb
  require 'capybara/poltergeist'
  require 'factory_bot_rails'
  require 'capybara/rspec'

  config.include Devise::Test::IntegrationHelpers, type: :feature
  config.include FactoryBot::Syntax::Methods
  Capybara.javascript_driver = :poltergeist
  Capybara.server = :puma

コードについて少し解説します。

requireメソッドによって、追加されたgemのファイルが読み込まれ、以後メソッドが使えるようになります。

config.include Devise::Test::IntegrationHelpers, type: :feature

上の設定は、capybaraテストでdeviseメソッドを使えるようにするためのものです。この設定をどこで知ったかというと、Deviseドキュメントに記載されています。

config.include FactoryBot::Syntax::Methods

上の設定は、factory_bot gemのメソッドを使うためのものです。この設定もgemのドキュメントで見つけました。

Capybara.javascript_driver = :poltergeist
Capybara.server = :puma

上の2つの設定は、capybaraでJavaScriptをテストできるようにするために必要です。実装方法でわからない点があれば、capybaraに限らず、まずドキュメントをお読みください。

テスト用gemと設定方法のほとんどを、個別ではなく一度に導入した理由は、今後問題が発生したときに備えてテストに必要なものについての全体像を明確にするためです。これで、このセクションに立ち戻ればテストの設定方法はほとんどここで確認できます。gemの導入や設定方法の記述があちこちにあると追うのが大変なのでこのセクションにまとめました。

変更をcommitし、いよいよテストを書くことにしましょう。

git add -A
git commit -m "
Set up the testing environment

- Remove test directory
- Add and configure rspec-rails, factory_bot_rails,
  rails-controller-testing, headless, capybara, poltergeist,
  database_cleaner gems"

Helper spec

個別のspec(テスト)の種類については、rspecのドキュメントrspec-rails gemのドキュメントをご覧ください。どちらも内容はだいたい同じですが、多少異なる点もあります。

新しいブランチを切ってそちらに切り替えます。

git checkout -b specs

まだヘルパーメソッドを1つ書いただけなので、これをテストしましょう。

specディレクトリに移動して、helpersディレクトリを作成します。

spec/helpers

このディレクトリにnavigation_helper_spec.rbファイルを作成します。

spec/helpers/navigation_helper_spec.rb

このファイルに以下のコードを書きます(Gist)。

# spec/helpers/navigation_helper_spec.rb
require 'rails_helper'

RSpec.describe NavigationHelper, :type => :helper do

end

require 'rails_helper'によって、すべてのテスト設定にアクセスできるようになります。:type => :helperは、helper spcを扱うことを指定し、これによって特定のメソッドが使えるようになります。

collapsible_links_partial_pathメソッドのテストを書いたnavigation_helper_spec.rbファイルは次のようになります(Gist)。

# spec/helpers/navigation_helper_spec.rb
require 'rails_helper'

RSpec.describe NavigationHelper, :type => :helper do

  context 'signed in user' do
    before(:each) { allow(controller).to receive(:user_signed_in?).and_return(true) }

    context '#collapsible_links_partial_path' do
      it "returns signed_in_links partial's path" do
        expect(helper.collapsible_links_partial_path).to (
          eq 'layouts/navigation/collapsible_elements/signed_in_links' )
      end
    end
  end

  context 'non-signed in user' do
    before(:each) { allow(controller).to receive(:user_signed_in?).and_return(false) }

    context '#collapsible_links_partial_path' do
      it "returns non_signed_in_links partial's path" do
        expect(helper.collapsible_links_partial_path).to (
          eq 'layouts/navigation/collapsible_elements/non_signed_in_links' )
      end
    end
  end

end

contextitについて詳しく学ぶには、RSpecのbasic structureドキュメントをご覧ください。上では「ユーザーがログインしている場合」と「ユーザーがログインしていない場合」の2つのケースについてテストしています。signed in userコンテキストとnon-signed in userコンテキストには、それぞれbeforeフックを記述しています。これらのフック(メソッド)は、それぞれのコンテキスト内でテスト実行前に前処理を実行します。ここではテスト実行前にstubメソッドを前処理として実行し、user_signed_in?の戻り値をどんな値にもできるようにします(ここではtrueを返す)。

訳注: rspeck-mocksのstubは現在非推奨です。元のコードbefore(:each) { helper.stubは、stubでdeprecation warningが表示されるため、allow(controller).to receiveに置き換えました(参考1)(参考2)。

そして最後に、expectメソッドを用いて、collapsible_links_partial_pathメソッドを呼び出したときの戻り値が期待どおりであることをチェックします。

以下を実行すれば、すべてのテストを実行できます。

rspec spec

navigation_helper_spec.rbファイルのテストだけを実行したい場合は、以下を実行します。

rspec spec/helpers/navigation_helper_spec.rb

テストがパスすれば、以下のように表示されます。

変更をcommitします。

git add -A
git commit -m "Add specs to NavigationHelper's collapsible_links_partial_path method"

ファクトリー

次に、テスト実行のためのサンプルデータが必要です。factory_bot gemを使って、サンプルデータが欲しいときにいつでも簡単に追加でき、ドキュメントも充実しているのでとても快適です。この時点のアプリで作成できるオブジェクトはUserのみです。userのファクトリーを定義するために、specディレクトリにfactoriesディレクトリを作成します。

spec/factories

factoriesディレクトリの下にusers.rbファイルを作成し、以下のコードを追加します(Gist)。

# spec/factories/users.rb
FactoryBot.define do
  factory :user do
    sequence(:name) { |n| "test#{n}" }
    sequence(:email) { |n| "test#{n}@test.com" }
    password '123456'
    password_confirmation '123456'
  end
end

これで、testデータベースでのユーザー作成をspec内でいつでも簡単にfactory_botのメソッドで行えるようになりました。ファクトリーの定義方法や使い方の完全なガイドについては、factory_bot gemのドキュメントをご覧ください。

userファクトリーの定義は割りと素直で、userオブジェクトに持たせる値を定義しています。sequenceメソッドは、ドキュメントを読むとわかるように、Userレコードを追加するたびにnの値が1つカウントアップします。たとえば最初のユーザー名はtest0、次のユーザー名はtest1という具合です。

変更をcommitします。

git add -A
git commit -m "add a users factory"

feature spec

feature specsには、アプリでのユーザー操作をシミュレーションするテストコードを書きます。capybara gemはfeature specを強化してくれます。

うれしいことに、最初のfeature specを書く準備はもう整っていますので、ログイン/ログアウト/サインアップ機能のテストを書くことにします。

specディレクトリの下にfeaturesディレクトリを作成します。

spec/features

featuresディレクトリの下に、さらにuserディレクトリを作成します。

spec/features/user

userディレクトリの下に、login_spec.rbファイルを作成します。

spec/features/user/login_spec.rb

ログインテストは次のようになります。

# spec/features/user/login_spec.rb
require "rails_helper"

RSpec.feature "Login", :type => :feature do
  let(:user) { create(:user) }

  scenario 'ユーザーがloginページにリダイレクトされ、ログインに成功する', js: true do
    user
    visit root_path
    find('nav a', text: 'Login').click
    fill_in 'user[email]', with: user.email
    fill_in 'user[password]', with: user.password
    find('.login-button').click
    expect(page).to have_selector('#user-settings')
  end

end

このテストコードは、ユーザーがhomeページを開くとloginページに移動するところをシミュレートします。続いてフォームに入力して送信し、最後に#user-settings要素(ユーザーがログイン中のときだけ現れる)がナビゲーションバーにあることをチェックします。

featurescenarioはCapybaraの構文です。featurecontextdescribeと同じであり、scenarioitと同じ機能です。詳しくはUsing Capybara With Rspecをご覧ください。

letを使うと、メモ化(memoize)されたメソッドを書いて、そのメソッドが定義されているcontextの内側にあるすべてのspecで利用できるようになります。

訳注: letについてはRSpecのletを使うのはどんなときか?(翻訳)もどうぞ。

ここでは、作成したuserファクトリーと、factory_bot gemのcreateメソッドも使っています。

js: true引数を指定すると、JavaScriptに関連する機能をテストできるようになります。

これまでと同様、login_spec.rbファイルを指定してテストを実行し、パスすることを確認します。

rspec spec/features/user/login_spec.rb

変更をcommitします。

git add -A
git commit -m "add login feature specs"

これで、ログアウト機能もテストできる状態になりました。userディレクトリの下にlogout_spec.rbファイルを作成します(Gist)。

spec/features/user/logout_spec.rb

テストの実装は次のようになります。

# spec/features/user/logout_spec.rb
require "rails_helper"

RSpec.feature "Logout", :type => :feature do
  let(:user) { create(:user) }

  scenario 'ユーザーがログアウトに成功する', js: true do
    sign_in user
    visit root_path
    find('nav #user-settings').click
    find('nav a', text: 'Log out').click
    expect(page).to have_selector('nav a', text: 'Login')
  end

end

このコードは、ユーザーが[Logout]ボタンをクリックするところをシミュレートし、ナビゲーションバーのユーザーのリンクがログインしていない状態になることをチェックします。

sign_inメソッドはDeviseのヘルパーメソッドです。こうしたヘルパーメソッドは、既にrails_helper.rbファイルでincludeしてあります。

テストを実行し、パスすることを確認します。

変更をcommitします。

git add -A
git commit -m "add logout feature specs"

残るは、新しいアカウントでのサインアップ機能です。これをテストしましょう。userディレクトリの下にsign_up_spec.rbファイルを作成します。テストは次のようになります(Gist)。

# spec/features/user/sign_up_spec.rb
require "rails_helper"

RSpec.feature "Sign up", :type => :feature do
  let(:user) { build(:user) }

  scenario 'ユーザーがsignupページを開いてサインアップに成功する', js: true do
    visit root_path
    find('nav a', text: 'Signup').click
    fill_in 'user[name]', with: user.name
    fill_in 'user[email]', with: user.email
    fill_in 'user[password]', with: user.password
    fill_in 'user[password_confirmation]', with: user.password_confirmation
    find('.sign-up-button').click
    expect(page).to have_selector('#user-settings')
  end

end

ここではsignupページを開いてフォームへの入力と送信をシミュレートし、#user-settings要素(ログイン中のユーザーにのみ現れる)が表示されることをチェックします。

createメソッドではなくDeviseのbuildメソッドを使うことで、データベースに保存せずに新しいオブジェクトを作成できます。

テストスイート全体を実行し、すべてパスことを確認します。

rspec spec

変更をcommitします。

git add -A
git commit -m "add sign up features specs"

最初のテストができあがりましたので、specsブランチをmasterにmergeします。

git checkout master
git merge specs

specsブランチが不要になったので削除します(q__o)。

git branch -D specs

関連記事

新しいRailsフロントエンド開発(1)Asset PipelineからWebpackへ(翻訳)

デザインも頼めるシステム開発会社をお探しならBPS株式会社までどうぞ 開発エンジニア積極採用中です! Ruby on Rails の開発なら実績豊富なBPS

この記事の著者

hachi8833

Twitter: @hachi8833、GitHub: @hachi8833 コボラー、ITコンサル、ローカライズ業界、Rails開発を経てTechRachoの編集・記事作成を担当。 これまでにRuby on Rails チュートリアル第2版の監修および半分程度を翻訳、Railsガイドの初期翻訳ではほぼすべてを翻訳。その後も折に触れて更新翻訳中。 かと思うと、正規表現の粋を尽くした日本語エラーチェックサービス enno.jpを運営。 実は最近Go言語が好きで、Goで書かれたRubyライクなGoby言語のメンテナーでもある。 仕事に関係ないすっとこブログ「あけてくれ」は2000年頃から多少の中断をはさんで継続、現在はnote.muに移転。

hachi8833の書いた記事

夏のTechRachoフェア2019

週刊Railsウォッチ

インフラ

ActiveSupport探訪シリーズ