1
3

More than 3 years have passed since last update.

Railsで作成したアプリケーションにRspec(E2Eテスト)実装してみた

Last updated at Posted at 2019-12-24

はじめに

本記事は、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がインストールできないので別の方法でインストールを実施します。

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をアンインストールしたくなりましたら下記コマンド実行してください。

chromeアンインストール
$ sudo yum --setopt=tsflags=noscripts -y remove google-chrome-stable
$ sudo rm -rf /opt/google/chrome/

chromedriverインストール

chromeインストール
$ 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へ設定を追記します。

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インストール
$ bundle install

RspecとCapybara設定の追加

Rails用のRspec初期ファイルを作成します。

Rspec初期ファイル作成
$ bundle exec rails g rspec:install
      create   .rspec
      create  spec/spec_helper.rb
      create  spec/rails_helper.rb

余談ですが、railsに組み込まないで使用する場合、下記コマンドでRspec初期ファイルを作成できます。

普通のRspec初期ファイル作成
$ bundle exec rspec --init
  create   .rspec
  create   spec/spec_helper.rb

Capybara用のファイルを作成します。

Capybaraファイル作成
$ mkdir -p spec/helpers

$ touch spec/helpers/capybara.rb

Capybaraの設定を追加します。

Capybara設定追加(capybara.rb)
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」の設定を変更します。

rails_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
spec_helper.rbの設定変更
【コメント部分は全部削除しています】

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接続して対話式にコマンドを実行する機能のテストを実施します。

モジュールテスト(ssh_interactive_spec.rb)
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アプリケーション(画面)のテストを実施します。

E2Eテスト(users_password_spec.rb)
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

テスト実行

最後に作成したテストツールを使用してテストを実行してみます。
※エラー結果も見せたいため、あえてエラーを発生させています。
 (嘘です。公開用の設定に変更したため環境周りでエラーが出ています)

テストコマンド実行(ssh_interactive_spec.rbのみ)
$ 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)は検出しない。

おわりに

テストツール自体はそれほど難しくありませんでしたが、環境や設定周りがすごく面倒でした。
また、今回はモデルを利用していなかったため「テストデータ作成」関連は手を付けていないため、
どこかで調査して記事にしていこうかなと考えています。

ただ、今回の記事では全てを理解したわけではないので、今後も気づいたことがあれば加筆・修正していく予定です。

1
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
3