18
16

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

System Specによる開発

Last updated at Posted at 2020-02-19

はじめに

こちらは社内技術勉強会用の資料として作成したものです。
Ruby on Railsにおいて、RSpecの機能である、System Specを使用して開発をしてみます。

サンプルプログラムを使用して、小さな開発を行いつつ、その動作確認をRSpecで行います。

サンプルプログラムについて

概要

サンプルプログラムUV Eatsは、Webサイト上で料理のメニューを注文すると配達してもらえる、というサービスであるとします。以下のような動作をするものとします。

  • あらかじめ登録されているユーザでログインして利用します。
  • メニュー一覧からひとつのメニューを選択して注文します。
  • 注文時は配達先住所、支払方法、クレジットカード番号を入力します。

今回使用する画面は以下の4つです。

  • ログイン画面 (Sessionsコントローラ)
  • メニュー画面 (Menusコントローラ)
  • 注文画面 (Ordersコントローラ)
  • 注文完了画面 (Ordersコントローラ)

サンプルプログラムのソースコードはこちらを参照してください。

画面イメージ

uveats_pages.png

DB

DBは以下の3つのテーブルで構成されています。

  • ユーザ(users)
  • メニュー(menus)
  • 注文(orders)

uveats tables.png

RSpecの使い方について

その1: テストコードの作成

テストコードは、specディレクトリ内に、specファイルを作成して記述します。ログイン処理のテストであれば、ログイン処理を実装しているSessionsコントローラに関連するものとして、spec/system/sessions_spec.rb に記述します。

ログイン画面を表示してみるコードは、以下のような内容になります。

spec/system/sessions_spec.rb
require 'rails_helper'

RSpec.describe "Sessions", type: :system, js: true do
  it 'ログイン画面が表示されること' do
    visit '/sessions/new'
  end
end

System Specは、describe のオプション type に、:system を指定することで記述します。

jsというオプションが指定されていますが、これはこのサンプルプログラムの都合によるものです。付加しておいてください。

テストの内容は、it のブロック内に記述していきます。

その2: 初期データの登録

例えば、サービスにログインできるようにするためには、あらかじめログイン可能なユーザが登録されている必要があります。あらかじめデータを登録しておく方法として、FactoryBot を使用します。

FactoryBotの使い方は、GETTING_STARTEDこちらの記事 を参照してください。

その3: RSpecの実行

RSpecを実行するには、以下のようにコマンドを実行します。$WEB_ROOT は、Railsプロジェクトのルートディレクトリであるとします。

cd $WEB_ROOT
bundle exec rspec -fd spec/system/sessions_spec.rb

以下のように表示されます。これは、1件のテストを実行し、0件が失敗した、ということを表しています。

1 example, 0 failures

その4: ページ内の文字列を検出

ログイン画面には、「メールアドレス」というラベルが表示されるとします。

ログイン画面が正しく表示されたかどうかを判定するには、URLにアクセスした後、このラベル文字列が含まれるページが表示されたかどうかを調べることでわかります。RSpecの構文で、以下のように記述します。

expect(page).to have_content('メールアドレス')

specファイル全体では以下のようになります。

spec/system/sessions_spec.rb
require 'rails_helper'

RSpec.describe "Sessions", type: :system, js: true do
  it 'ログイン画面が表示されること' do
    visit '/sessions/new'
    expect(page).to have_content('メールアドレス')
  end
end

CapybaraによるWebブラウザの操作方法

その1: Webページへのアクセス

System Specは、自動でWebブラウザを操作してWebアプリケーションにアクセスし、動作の確認を行う仕組みです。Webブラウザの操作には、Capybara を使用します。

Capybaraの構文を使用して、Webページにアクセスするには visit メソッドを使用します。

例えば、ログインページのパスが /sessions/new であったとすると、以下のように記述することで、ログインページにアクセスすることができます。

visit '/sessions/new'

その2: テキストフィールドへの入力

たとえばメニューを注文する画面において、配達先住所の入力欄が以下のように定義されていたとします。

<label for="order_delivery_address">配達先住所</label>
<input type="text" id="order_delivery_address" name="order[delivery_address]" value="">

この要素に対して「東京都新宿区内藤町11番地」と入力したい場合は、Capybaraの fill_in メソッドを使用し、以下のいずれかのように記述します。

# 入力対象をラベルで指定する場合
fill_in '配達先住所', with: '東京都新宿区内藤町11番地'
# 入力対象をid属性で指定する場合
fill_in 'order_delivery_address', with: '東京都新宿区内藤町11番地'
# 入力対象をname属性で指定する場合
fill_in 'order[delivery_address]', with: '東京都新宿区内藤町11番地'

その3: ドロップダウンの項目の選択

メニューを注文する画面において、支払方法の洗濯欄が以下のように定義されていたとします。

<select name="order[payment_method]">
  <option value="01">現金</option>
  <option value="02">クレジットカード</option>
</select>

このドロップダウンで「クレジットカード」を選択したい場合は、Capybaraの select メソッドを使用して、以下のように記述します。

select 'クレジットカード', from: 'order[payment_method]'

選択したい項目はvalueではなく、テキストで指定していることに注意してください。

その4: ボタンのクリック

メニューを注文する画面において、入力フォームの内容を送信するボタンが以下のように定義されているとします。

<input type="submit" value="注文する">

このボタンをクリックするには、click_button メソッドで、以下のように記述します。

click_button '注文する'

リンクをクリックしたい場合や、ボタンとリンクを区別したくない場合などについては、Clicking links and buttons を参照してください。

課題の実施にあたって

RSpecとCapybaraの簡単な使い方を確認したところで、課題を始めましょう。ここで、みなさんに無理難題を申し付けます。

課題にあたっては、ローカルPCで普段使用しているWebブラウザを使用しないでください。使用してもよいWebブラウザは、dockerコンテナ内にインストールされているheadless chromeのみとします。

ページがどのように表示されているのか分からなくて困る?まぁ、とりあえずはじめましょう。

課題1

ログイン画面が表示されることを確認してください。

説明

ログイン画面は、サンプルプログラムではSessionsコントローラ、newアクションに実装されています。Webブラウザでこのアクションに向かってアクセスし、ログイン画面が表示されることを確認してください。

Hint 1

ログイン画面を表示するためのテストコードは、前述「RSpecの使い方について」の「その4: ページ内の文字列を検出」に記載されています。実際にテストコードをspecファイルに記述して実行してみてください。

Hint 2

パスを固定文字列で直接指定してもかまいませんが、Sessionsコントローラ、newアクションにアクセスするためのURL(またはパス)は、URLヘルパーメソッド new_session_urlnew_session_path で取得することができます。これらのヘルパーメソッドでURL部分を置き換えてみましょう。

Hint 3

headless chromeの画面を直接見ることはできませんが、必要なタイミングでスクリーンショットを作成することができます。スクリーンショットを作成するには以下のように記述します。

take_screenshot

ログイン画面にアクセスした後に、スクリーンショットを作成してみましょう。テストコード内、visit メソッド呼び出しの次の行に追加して、実行してみてください。

スクリーンショットは tmp/screenshots/ というディレクトリに、png形式のファイルで作成されます。フォルダウィンドウでこのフォルダを開いておき、画像がプレビュー表示される状態にしておくとよいでしょう。

uveats_login.png

また、スクリーンショットではなく、生成されたHTMLを確認したい場合は、visit メソッド呼び出しの後に以下のように記述しておくと、RSpecを実行しているターミナルの画面に出力されます。

puts page.body

課題2

ログイン画面にメールアドレスとパスワードを入力し、ログインを成功させてください。

説明

ログイン画面にはメールアドレス、パスワードの2つの入力欄と、ログインボタンがあります。DBに存在するユーザのメールアドレスとパスワードを入力し、ログインボタンをクリックしてください。

Hint 1

テスト用のデータベースには、最初はレコードが1件も登録されていない状態です。ログインするためには、あらかじめユーザを1件登録しておく必要があります。

ユーザのレコードは、FactoryBotを使用すると、以下のようなコードで登録することができます。

user = create(:user)

user変数にUserモデルのインスタンスが代入されます。このインスタンスにはFactoryBotによって自動的に生成されたメールアドレスとパスワードがセットされています。それぞれ、user.mail、user.password とすると値を参照することができます。これらの値を、入力欄に与えてみましょう。

Hint 2

入力フォームのへの入力とボタンのクリックは、前述のように、fill_inclick_button で行うことができます。

Hint 3

ログインに成功すると、メニュー一覧画面が表示されます。ログインに成功したかどうかは、スクリーンショットを作成する、または、ページの内容にMenusという文字が含まれているかどうかを判定する、などの方法により確認することができます。

課題3

注文画面を完成させてください。

説明

ユーザがWebサイトにログインし、メニュー画面でいずれかのメニューを選択したとします。すると、注文画面が表示されます。

uveats_order.png

この注文画面のビューとコントローラの開発を行ってください。

注文画面には以下の入力項目があるとします。これらの項目がある入力フォームをビュー(app/views/orders/_form.html.erb)に追加してください。

入力項目 物理名 入力方法 選択肢
配達先住所 delivery_address テキスト -
支払方法 payment_method ドロップダウン 01:現金、02:クレジットカード
カード番号 card_number テキスト -

Ordersコントローラ(app/controllers/orders_controller.rb)では、createアクションで注文画面から送信された内容を受け取り、Orderモデルで、DBのordersテーブルにレコードを登録します。

Orderモデルのインスタンスを生成するには、以下の属性値を指定します。

属性名 物理名 値の取り出し元
メニューID menu_id 数値 params[:menu_id]
ユーザID user_id 数値 @me.id
配達先住所 delivery_address テキスト paramsorder内、delivery_address
支払方法 payment_method ドロップダウン paramsorder内、payment_method
カード番号 card_number テキスト paramsorder内、card_number

Ordersコントローラのorder_paramsメソッドに、上記の属性値を取り出すコードを記述してください。

テストコードは、spec/system/orders_spec.rbに記述してください。

Hint 1

ビューファイル _form.html.erb 内での、テキスト入力欄、送信ボタンなどの記述方法は、menususersなどのビューファイルを参考にしてください。

ドロップダウンの記述方法は、Railsガイドの SelectタグとOptionタグ を参考にしてください。以下のようなコードになります。

<%= form.select :payment_method, options_for_select([['現金', '01'], ['クレジットカード', '02']]) %>

Hint 2

どのようなHTTPリクエストが送信されているかを確認したくなった場合は、Railsのログを見ることで確認することができます。ログは log/test.log に出力されます。たとえば、注文画面での入力内容は以下のように送信されていることがわかります。

Started POST "/orders?menu_id=11" for 127.0.0.1 at 2020-02-18 23:38:23 +0900
Processing by OrdersController#create as HTML
  Parameters: {"order"=>{"delivery_address"=>"MyText1", "payment_method"=>"01", "card_number"=>"MyText1"}, "commit"=>"登録する", "menu_id"=>"11"}

新しいターミナルを開き、以下のコマンドを実行して、常にログを確認できる状態にしておくとよいでしょう。

tail -f log/test.log

Hint 3

注文を行うためには、あらかじめDBに以下のデータが必要です。

  • ログインする(=注文する)ユーザ
  • 注文するメニュー

このうち、ログインするユーザは、spec/system/orders_spec.rb内の以下の行により、自動的に登録されるようになっています。

  include_context :system_shared_context

メニューは、FactorbyBotを使用して、以下のように記述すると登録することができます。

create(:menu)

データを登録したら、Capybaraを使用して入力欄を埋め、「登録する」ボタンをクリックしてください。

課題4 おまけ

サンプルプログラムでは、注文画面において、支払方法に「クレジットカード」、カード番号に「0000」を入力すると、決済に失敗したとして例外が発生するようになっています。

例外を検知し、例外のメッセージと共に注文画面を再表示するようにプログラムを修正してください。

課題1の解答例

spec/system/sessions_spec.rb
require 'rails_helper'

RSpec.describe "Sessions", type: :system, js: true do
  it 'ログイン画面が表示されること' do
    # ここにテストコードを書く
    visit new_session_path
    take_screenshot
  end
end

課題2の解答例

spec/system/sessions_spec.rb
require 'rails_helper'

RSpec.describe "Sessions", type: :system, js: true do
  it 'ログインに成功すること' do
    # ここにテストコードを書く
    login_user = create(:user)
    visit new_session_path
    fill_in 'user[mail]', with: login_user.mail
    fill_in 'user[password]', with: login_user.password
    click_button 'ログイン'
    expect(page).to have_content('Menus')
  end
end

letを使って書く場合は以下のようになります。

spec/system/sessions_spec.rb
require 'rails_helper'

RSpec.describe "Sessions", type: :system, js: true do
  describe 'ログインに' do
    let (:login_user) { create(:user) }

    it '成功すること' do
      visit new_session_path
      fill_in 'user[mail]', with: login_user.mail
      fill_in 'user[password]', with: login_user.password
      click_button 'ログイン'
      expect(page).to have_content('Menus')
    end
  end
end

課題3の解答例

ビュー

app/views/orders/_form.html.erb
  <div class="field">
    <%= form.label :delivery_address %>
    <%= form.text_field :delivery_address %>
  </div>

  <div class="field">
    <%= form.label :payment_method %>
    <%= form.select :payment_method, options_for_select([['現金', '01'], ['クレジットカード', '02']]) %>
  </div>

  <div class="field">
    <%= form.label :card_number %>
    <%= form.text_field :card_number %>
  </div>

  <div class="actions">
    <%= form.submit nil, { class: 'button' } %>
  </div>

コントローラ

app/controllers/orders_controller.rb
    def order_params
      params.require(:order).permit(
        :delivery_address,
        :payment_method,
        :card_number,
      ).merge({
        menu_id: params[:menu_id]
      }).merge({
        user_id: @me.id
      })
    end

テストコード

spec/system/orders_spec.rb
require 'rails_helper'

RSpec.describe "Orders", type: :system, js: true do
  include_context :system_shared_context

  # ここにテストコードを書く
  describe '新規登録に' do
    let (:menu) { create(:menu) }
    let (:order) { build(:order) }
    let (:payment_trans) { { '01' => '現金', '02' => 'クレジットカード' } }

    it '成功すること' do
      visit new_order_path({ menu_id: menu.id })
      fill_in 'order[delivery_address]', with: order.delivery_address
      select payment_trans[order.payment_method], from: 'order[payment_method]'
      fill_in 'order[card_number]', with: order.card_number
      click_button '登録する'
      expect(page).to have_content('Order was successfully created.')
    end
  end
end

おわりに

開発において、DBの値のリセットや入力フォームへの値の入力などの作業をテストコードに任せる、ということを体験していただくことができましたでしょうか?

今回はRuby on RailsとSystem Specでしたが、他の開発言語、フレームワークでも同様なことができるものもあるでしょう。

テストコードはテストを行う手段というだけでなく、開発時にも使ってみていただけるとよいと思います。

18
16
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
18
16

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?