はじめに
本記事は、Railsのテストフレームワーク(Rspec)の手順をまとめたものになります。
また、当記事は以前の投稿記事「Railsでセルフパスワード変更ページを作ってみた」で作成した環境を前提に作成していますので、参考にされる方はご注意ください。
前提条件
- 開発環境はCloud9
- Linuxコマンドの使い方がわかる程度の力量
- Web開発は初学者クラスの力量(ruby on rails開発未経験/Progateのレッスンは修了済)
- Rspec初学者クラスの力量
参考記事
RSpecとCapybaraを使ってE2Eテストの土台を作ってみる
RailsでRSpecの初期設定を行う際のテンプレートを作ってみる
Rspecの設定はrails_helper.rbにだけ書けばよい
RSpecの初期設定メモ
Railsでrspecを使うように設定する
実用的な新機能が盛りだくさん!RSpec 3.3 完全ガイド
RSpec 設定
RSpecコトハジメ ~初期設定マニュアル~
使えるRSpec入門・その1「RSpecの基本的な構文や便利な機能を理解する」
既存のRailsプロジェクトをRSpec 3.0にアップグレードする際の注意点 ~RSpec 3は怖くないよ!~
特定のSpecでだけトランザクションのロールバックを無効にする
rspec-rails 3.7の新機能!System Specを使ってみた
SeleniumからHeadless Chromeを使ってみた
Amazon LinuxでSelenium環境を最短で構築する
RSpecの(describe/context/example/it)の使い分け
RSpec の letとlet!とbeforeの挙動と実行される順番
Ruby on Rails アプリケーションにおけるモンキーパッチの当て方【外部サイト】
rails generate rspec:install時に生成されるhelperの設定【外部サイト】
RailsでのRSpec実行時乱数の状態を実行毎に変わらないように固定する【外部サイト】
Rspec入門編ーテストコードを書いてみよう!ー【外部サイト】
RSpec 3 時代の設定ファイル rails_helper.rb について【外部サイト】
RSpec 3.5 から shared_context の使い方が少し変わっていた [RSpec]【外部サイト】
RSpecのshared_contextで共通処理を1ヶ所にまとめる【外部サイト】
必要ソフトウェア
- ブラウザエミュレート環境インストール
- chrome(ブラウザ)
- GConf2(chromedriver用)
- chromedriver
- gemのインストール
- rspec-rails
- webdrivers
- selenium-webdriver
- capybara
ディレクトリ構成
今回のプロジェクトでは下記ディレクトリ構成となります。
spec
├── controllers
│ └── users_password_controller_spec.rb
├── helpers
│ └── capybara.rb
├── lib
│ └── ssh_interactive_spec.rb
├── rails_helper.rb
├── spec_helper.rb
└── system
└── users_password_spec.rb
Selenium環境構築
chromeインストール
現在の環境がyumで新しいchromeがインストールできないので別の方法でインストールを実施します。
$ sudo curl https://intoli.com/install-google-chrome.sh | bash
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
100 9526 100 9526 0 0 14260 0 --:--:-- --:--:-- --:--:-- 14239
Working in /tmp/google-chrome-installation
【以下省略】
Extracting graphite2...
Successfully installed google-chrome-stable, Google Chrome 78.0.3904.108 .
余談ですが、chromeをアンインストールしたくなりましたら下記コマンド実行してください。
$ sudo yum --setopt=tsflags=noscripts -y remove google-chrome-stable
$ sudo rm -rf /opt/google/chrome/
chromedriverインストール
$ sudo yum -y install GConf2
Loaded plugins: priorities, update-motd, upgrade-helper
You need to be root to perform this command.
【以下省略】
Installed:
GConf2.x86_64 0:2.28.0-7.el6
Dependency Installed:
ConsoleKit.x86_64 0:0.4.1-6.el6 ConsoleKit-libs.x86_64 0:0.4.1-6.el6 ORBit2.x86_64 0:2.14.17-7.el6 dbus-glib.x86_64 0:0.86-6.10.amzn1 eggdbus.x86_64 0:0.6-3.el6 libIDL.x86_64 0:0.8.13-2.1.4.amzn1
polkit.x86_64 0:0.96-11.el6_10.1 sgml-common.noarch 0:0.6.3-33.5.amzn1
Complete!
gemインストール
Gemfileへ設定を追記します。
$ cd PasswordChange
$ vim Gemfile
【変更前】
group :development, :test do
# Call 'byebug' anywhere in the code to stop execution and get a debugger console
gem 'byebug', platforms: [:mri, :mingw, :x64_mingw]
end
【変更後】
group :development, :test do
# Call 'byebug' anywhere in the code to stop execution and get a debugger console
gem 'byebug', platforms: [:mri, :mingw, :x64_mingw]
gem 'selenium-webdriver'
gem 'webdrivers'
gem 'capybara'
gem 'rspec-rails'
end
bundleインストールを実行します。
$ bundle install
RspecとCapybara設定の追加
Rails用のRspec初期ファイルを作成します。
$ bundle exec rails g rspec:install
create .rspec
create spec/spec_helper.rb
create spec/rails_helper.rb
余談ですが、railsに組み込まないで使用する場合、下記コマンドでRspec初期ファイルを作成できます。
$ bundle exec rspec --init
create .rspec
create spec/spec_helper.rb
Capybara用のファイルを作成します。
$ mkdir -p spec/helpers
$ touch spec/helpers/capybara.rb
Capybaraの設定を追加します。
require 'capybara/rspec'
require 'selenium-webdriver'
RSpec.configure do |config|
config.include Capybara::DSL
# javascript無
config.before(:each, type: :system) do
driven_by :rack_test
end
# javascript有
config.before(:each, type: :system, js: true) do
driven_by :selenium_chrome_headless, screen_size: [1280, 800], options: {
browser: :chrome
} do |driver_option|
# Chrome オプション追加設定
driver_option.add_argument('disable-notifications')
driver_option.add_argument('disable-translate')
driver_option.add_argument('disable-extensions')
driver_option.add_argument('disable-infobars')
driver_option.add_argument('disable-gpu')
driver_option.add_argument('no-sandbox')
driver_option.add_argument('lang=ja')
driver_option.add_argument('headless')
end
# Capybara設定
Capybara.javascript_driver = :selenium_chrome_headless
Capybara.run_server = true
Capybara.default_selector = :css
Capybara.default_max_wait_time = 5
Capybara.ignore_hidden_elements = true
end
end
次に「rails_helper.rb」「spec_helper.rb」の設定を変更します。
【コメント部分は全部削除しています】
require 'spec_helper'
ENV['RAILS_ENV'] ||= 'test'
require File.expand_path('../config/environment', __dir__)
abort("The Rails environment is running in production mode!") if Rails.env.production?
require 'rspec/rails'
begin
ActiveRecord::Migration.maintain_test_schema!
rescue ActiveRecord::PendingMigrationError => e
puts e.to_s.strip
exit 1
end
RSpec.configure do |config|
config.fixture_path = "#{::Rails.root}/spec/fixtures"
config.use_transactional_fixtures = true
config.infer_spec_type_from_file_location!
config.filter_rails_from_backtrace!
end
【コメント部分は全部削除しています】
require 'helpers/capybara.rb'
RSpec.configure do |config|
config.expect_with :rspec do |expectations|
expectations.include_chain_clauses_in_custom_matcher_descriptions = true
end
config.mock_with :rspec do |mocks|
mocks.verify_partial_doubles = true
end
config.shared_context_metadata_behavior = :apply_to_host_groups
config.failure_color = :red
config.fail_fast = false
config.color = true
config.formatter = :documentation
テストコード記述
テストコード保管先とテストコード用のファイルを作成します。
$ mkdir -p spec/lib spec/system
$ touch spec/lib/ssh_interactive_spec.rb spec/lib/users_password_spec.rb
実際にテストコードを記述します。
自作モジュールテスト(Rspec)
SSH接続して対話式にコマンドを実行する機能のテストを実施します。
require 'rails_helper'
require 'ssh_interactive'
require 'settings'
RSpec.describe SshInteractive do
describe "SSH対話式(passwdコマンド)" do
let!(:user_id) {'ユーザー名'}
let!(:pass_old) {'現行パスワード'}
let!(:pass_new) {'新しいパスワード'}
let!(:host) {Settings.ssh_params.host}
let!(:port) {Settings.ssh_params.port}
let!(:keys) {Settings.ssh_params.keys}
let!(:passphrase) {Settings.ssh_params.passphrase}
let!(:ssh) { SshInteractive.new }
context 'SSH接続先の情報が誤っている場合' do
# 前処理
before do
ssh.set_host(host)
ssh.set_port(22)
ssh.set_publickey_auth(keys, passphrase)
end
it 'SSH接続エラーが発生する事' do
result = ssh.password_change(user_id, pass_old, pass_new , pass_new)
expect(result).to eq (-10)
end
end
context '存在しないユーザーが設定されている場合' do
# 前処理
before do
ssh.set_host(host)
ssh.set_port(port)
ssh.set_publickey_auth(keys, passphrase)
end
it 'SSH接続エラーが発生する事' do
result = ssh.password_change('test1', pass_old, pass_new , pass_new)
expect(result).to eq (-1)
end
end
context '旧(現行)パスワード間違っている場合' do
# 前処理
before do
ssh.set_host(host)
ssh.set_port(port)
ssh.set_publickey_auth(keys, passphrase)
end
it '認証エラーが発生する事' do
result = ssh.password_change(user_id, pass_old + 'dgh', pass_new , pass_new)
expect(result).to eq (-2)
end
end
context '辞書攻撃チェックに引っかかった場合' do
# 前処理
before do
ssh.set_host(host)
ssh.set_port(port)
ssh.set_publickey_auth(keys, passphrase)
end
it 'パスワード変更エラーが発生する事' do
result = ssh.password_change(user_id, pass_old, pass_new + 'abcdefghijk' , pass_new + 'abcdefghijk')
expect(result).to eq (-3)
end
end
context '全ての条件がクリアされている場合' do
# 前処理
before do
ssh.set_host(host)
ssh.set_port(port)
ssh.set_publickey_auth(keys, passphrase)
end
it 'パスワード変更処理が正常に完了する事' do
result = ssh.password_change(user_id, pass_old, pass_new, pass_new)
expect(result).to eq (0)
end
end
end
end
E2Eテスト(System Spec + Selenium)
Webアプリケーション(画面)のテストを実施します。
require 'rails_helper'
RSpec.describe 'top_form', type: :system, js: true do
describe '/top' do
let!(:save_path) {'tmp/screenshots/'}
let!(:user_id) {'test'}
let!(:pass_old) {'_8Ac-E5s'}
let!(:pass_new) {'X9a@ywV5'}
before do
visit '/top'
end
context '存在しないユーザーが入力されている場合' do
before do
find('#user_id').set(user_id + '1')
find('#password_old').set(pass_old)
find('#password_new').set(pass_new)
find('#password_verify').set(pass_new)
find_button('変更').click
end
it '警告メッセージが表示される事' do
expect(page).to have_content "ユーザーID又はパスワードが間違っています"
end
after do
page.driver.save_screenshot(save_path + 'page_top_case01.png')
end
end
context '旧(現行)パスワード間違っている場合' do
before do
find('#user_id').set(user_id)
find('#password_old').set(pass_old + 'abc')
find('#password_new').set(pass_new)
find('#password_verify').set(pass_new)
find_button('変更').click
end
it "警告メッセージが表示される事" do
expect(page).to have_content "パスワードが間違っています"
end
after do
page.driver.save_screenshot(save_path + 'page_top_case02.png')
end
end
context '旧(現行)パスワードと新しいパスワードが一致する場合' do
before do
find('#user_id').set(user_id)
find('#password_old').set(pass_old)
find('#password_new').set(pass_old)
find('#password_verify').set(pass_new)
find_button('変更').click
end
it "警告メッセージが表示される事" do
expect(page).to have_content "古いパスワードと新しいパスワードが同じです"
end
after do
page.driver.save_screenshot(save_path + 'page_top_case03.png')
end
end
context '新しいパスワードと新しいパスワード(確認)が一致しない場合' do
before do
find('#user_id').set(user_id)
find('#password_old').set(pass_old)
find('#password_new').set(pass_new)
find('#password_verify').set(pass_new + 'abc' )
find_button('変更').click
end
it '警告メッセージが表示される事' do
expect(page).to have_content "新しいパスワードと新しいパスワード"
end
after do
page.driver.save_screenshot(save_path + 'page_top_case04.png')
end
end
context '新しいパスワードが複雑性を満たすパスワードではない場合(数字無)' do
before do
find('#user_id').set(user_id)
find('#password_old').set(pass_old)
find('#password_new').set('Xda@ywVb')
find('#password_verify').set('Xda@ywVb')
find_button('変更').click
end
it '警告メッセージが表示される事' do
expect(page).to have_content "複雑性を満たすパスワードになっていません"
end
after do
page.driver.save_screenshot(save_path + 'page_top_case05.png')
end
end
context '新しいパスワードが複雑性を満たすパスワードではない場合(英小文字無)' do
before do
find('#user_id').set(user_id)
find('#password_old').set(pass_old)
find('#password_new').set('X9A@YWV5')
find('#password_verify').set('X9A@YWV5')
find_button('変更').click
end
it '警告メッセージが表示される事' do
expect(page).to have_content "複雑性を満たすパスワードになっていません"
end
after do
page.driver.save_screenshot(save_path + 'page_top_case06.png')
end
end
context '新しいパスワードが複雑性を満たすパスワードではない場合(英大文字無)' do
before do
find('#user_id').set(user_id)
find('#password_old').set(pass_old)
find('#password_new').set('x9a@ywwv5')
find('#password_verify').set('x9a@ywwv5')
find_button('変更').click
end
it '警告メッセージが表示される事' do
expect(page).to have_content "複雑性を満たすパスワードになっていません"
end
after do
page.driver.save_screenshot(save_path + 'page_top_case07.png')
end
end
context '新しいパスワードが複雑性を満たすパスワードではない場合(対象記号無)' do
before do
find('#user_id').set(user_id)
find('#password_old').set(pass_old)
find('#password_new').set('X9aywV5')
find('#password_verify').set('X9aywV5')
find_button('変更').click
end
it '警告メッセージが表示される事' do
expect(page).to have_content "複雑性を満たすパスワードになっていません"
end
after do
page.driver.save_screenshot(save_path + 'page_top_case08.png')
end
end
context '新しいパスワードの文字数が基準を満たしていない場合(7文字以下)' do
before do
find('#user_id').set(user_id)
find('#password_old').set(pass_old)
find('#password_new').set('X9a@ywV')
find('#password_verify').set('X9a@ywV')
find_button('変更').click
end
it '警告メッセージが表示される事' do
expect(page).to have_content "パスワードの文字数が基準を満たしていません"
end
after do
page.driver.save_screenshot(save_path + 'page_top_case09.png')
end
end
context '新しいパスワード内にユーザー名が含まれている場合' do
before do
find('#user_id').set(user_id)
find('#password_old').set(pass_old)
find('#password_new').set('X9a@ywV5az74d52_test')
find('#password_verify').set('X9a@ywV5az74d52_test')
find_button('変更').click
end
it '警告メッセージが表示される事' do
expect(page).to have_content "新しいパスワードにユーザーIDと同じ文字列が含まれています"
end
after do
page.driver.save_screenshot(save_path + 'page_top_case10.png')
end
end
context 'パスワード内に辞書攻撃に該当する文字列が含まれいている場合' do
before do
find('#user_id').set(user_id)
find('#password_old').set(pass_old)
find('#password_new').set(pass_new + 'abcdefghijk')
find('#password_verify').set(pass_new + 'abcdefghijk')
find_button('変更').click
end
it '警告メッセージが表示される事' do
expect(page).to have_content "辞書攻撃チェックに該当します"
end
after do
page.driver.save_screenshot(save_path + 'page_top_case11.png')
end
end
context '全ての条件がクリアされている場合' do
before do
find('#user_id').set(user_id)
find('#password_old').set(pass_old)
find('#password_new').set(pass_new)
find('#password_verify').set(pass_new)
find_button('変更').click
end
it 'パスワード変更完了画面が表示される事' do
expect(page).to have_content "パスワード変更が完了しました"
end
after do
page.driver.save_screenshot(save_path + 'page_top_normal.png')
end
end
end
end
テスト実行
最後に作成したテストツールを使用してテストを実行してみます。
※エラー結果も見せたいため、あえてエラーを発生させています。
(嘘です。公開用の設定に変更したため環境周りでエラーが出ています)
$ bundle exec rspec spec/lib/ssh_interactive_spec.rb
SshInteractive
SSH対話式(passwdコマンド)
SSH接続先の情報が誤っている場合
SSH接続エラーが発生する事
存在しないユーザーが設定されている場合
SSH接続エラーが発生する事
旧(現行)パスワード間違っている場合
認証エラーが発生する事 (FAILED - 1)
辞書攻撃チェックに引っかかった場合
パスワード変更エラーが発生する事 (FAILED - 2)
全ての条件がクリアされている場合
パスワード変更処理が正常に完了する事 (FAILED - 3)
Failures:
1) SshInteractive SSH対話式(passwdコマンド) 旧(現行)パスワード間違っている場合 認証エラーが発生する事
Failure/Error: expect(result).to eq (-2)
expected: -2
got: -1
(compared using ==)
# ./spec/lib/ssh_interactive_spec.rb:59:in `block (4 levels) in <top (required)>'
2) SshInteractive SSH対話式(passwdコマンド) 辞書攻撃チェックに引っかかった場合 パスワード変更エラーが発生する事
Failure/Error: expect(result).to eq (-3)
expected: -3
got: -1
(compared using ==)
# ./spec/lib/ssh_interactive_spec.rb:74:in `block (4 levels) in <top (required)>'
3) SshInteractive SSH対話式(passwdコマンド) 全ての条件がクリアされている場合 パスワード変更処理が正常に完了する事
Failure/Error: expect(result).to eq (0)
expected: 0
got: -1
(compared using ==)
# ./spec/lib/ssh_interactive_spec.rb:90:in `block (4 levels) in <top (required)>'
Finished in 0.32911 seconds (files took 2.54 seconds to load)
5 examples, 3 failures
ここまでが今回の記事におけるRspec実装の内容になります。
下記以降はRspec実装に当たっての各種参考設定の情報になります。
【参考設定情報】
下記ディレクトリ構成は公式ドキュメント RSpec Rails 3-9 【外部サイト】からの引用となります。
作成時点では、このディレクトリ構造を知らなかったのであまり守られていないです。
ディレクトリ構成
app
├── controllers
│ ├── application_controller.rb
│ └── books_controller.rb
├── helpers
│ ├── application_helper.rb
│ └── books_helper.rb
├── models
│ ├── author.rb
│ ├── book.rb
└── views
├── books
├── layouts
lib
├── country_map.rb
├── development_mail_interceptor.rb
├── enviroment_mail_interceptor.rb
└── tasks
├── irc.rake
spec
├── controllers
│ ├── books_controller_spec.rb
├── country_map_spec.rb
├── features
│ ├── tracking_book_delivery_spec.rb
├── helpers
│ └── books_helper_spec.rb
├── models
│ ├── author_spec.rb
│ ├── book_spec.rb
├── rails_helper.rb
├── requests
│ ├── books_spec.rb
├── routing
│ └── books_routing_spec.rb
├── spec_helper.rb
└── tasks
│ ├── irc_spec.rb
└── views
├── books
設定ファイルのメモ
spec_helper.rbの設定内容
Rspecに関する設定を書くための設定ファイルらしいです。
config設定 | 概要 |
---|---|
config.filter_run_when_matching :focus | 「rspec --tag focus」コマンド実行時に「:focus」タグが設定されたテストを実行。 |
config.run_all_when_everything_filtered = true | 「:focus」タグがついたものが何もない場合、フィルタを無視。 |
config.example_status_persistence_file_path = "spec/examples.txt" | 「rspec --only-failures」コマンド実行時にテスト結果の一時保管。 |
config.disable_monkey_patching! | モンキーパッチを無効化。 |
config.profile_examples = 10 | 実行後、遅いテスト項目を表示(10件)。 |
config.order = :random | 実行結果の順番(random = ランダム) |
Kernel.srand config.seed | 「rspec --seed 【seed値】」コマンド実行時にランダム結果を固定。 |
config.failure_color = :red | 実行結果のエラー内容をカラー出力(red = 赤色) |
config.fail_fast = false | 実行中にエラーが発生時の処理(false = 最後までテスト実施) |
config.color = true | 実行結果の標準出力をカラー出力 |
config.formatter = :documentation | 実行結果のフォーマット指定(デフォルト:progress) |
config.shared_context_metadata_behavior = :apply_to_host_groups | shared_contextの記述指定(:trigger_inclusionは3.4以前の書き方)。 |
rails_helper.rbの設定内容
Rails特有の設定を書くための設定ファイルらしいです。
config設定 | 概要 |
---|---|
config.infer_spec_type_from_file_location! | specファイルが配置されているディレクトリ内のspecタイプ(model, controller, feature等)を自動判別。 |
config.filter_rails_from_backtrace! | backtrace表示を簡素化。 |
config.use_transactional_fixtures = true | テスト実行後、データベースのデータ削除。 |
Capybaraの設定内容
RubyでWebアプリケーションのE2Eテストフレームワークを提供する機能らしいです。
capybara.rb設定 | 概要 |
---|---|
config.include Capybara::DSL | Capybaraを取り込むのに必要な設定。 |
driven_by :rack_test | rackアプリケーションをテストするための機能。javascriptのテスト不可。 |
driven_by :selenium_chrome_headless | Rubyクライアントライブラリを使用してChromeブラウザでテストするための機能。javascriptのテスト可。 |
driver_option.add_argument('disable-notifications') | Web通知やPush APIによる通知を無視。 |
driver_option.add_argument('disable-translate') | 翻訳ツールバーを無効。 |
driver_option.add_argument('disable-extensions') | 拡張機能を無効。 |
driver_option.add_argument('disable-infobars') | インフォバーの表示を無効。 |
driver_option.add_argument('disable-gpu') | GPU描画処理を無効。 |
driver_option.add_argument('no-sandbox') | sandboxを無効。 |
driver_option.add_argument('lang=ja') | 言語を日本語。 |
driver_option.add_argument('headless') | Headlessモード(画面を表示せずに動作)を有効。 |
Capybara.javascript_driver = :selenium_chrome_headless | chromeのheadlessモードでjavascript実行。 |
Capybara.run_server = true | rackサーバーを実行。 |
Capybara.default_selector = :css | セレクタ(find等)利用時の設定。 |
Capybara.default_max_wait_time = 5 | ajaxやcss等の待ち時間。 |
Capybara.ignore_hidden_elements = true | 非表示要素(display:none)は検出しない。 |
おわりに
テストツール自体はそれほど難しくありませんでしたが、環境や設定周りがすごく面倒でした。
また、今回はモデルを利用していなかったため「テストデータ作成」関連は手を付けていないため、
どこかで調査して記事にしていこうかなと考えています。
ただ、今回の記事では全てを理解したわけではないので、今後も気づいたことがあれば加筆・修正していく予定です。