はじめに
こちらは社内技術勉強会用の資料として作成したものです。
Ruby on Railsにおいて、RSpecの機能である、System Specを使用して開発をしてみます。
サンプルプログラムを使用して、小さな開発を行いつつ、その動作確認をRSpecで行います。
サンプルプログラムについて
概要
サンプルプログラムUV Eatsは、Webサイト上で料理のメニューを注文すると配達してもらえる、というサービスであるとします。以下のような動作をするものとします。
- あらかじめ登録されているユーザでログインして利用します。
- メニュー一覧からひとつのメニューを選択して注文します。
- 注文時は配達先住所、支払方法、クレジットカード番号を入力します。
今回使用する画面は以下の4つです。
- ログイン画面 (Sessionsコントローラ)
- メニュー画面 (Menusコントローラ)
- 注文画面 (Ordersコントローラ)
- 注文完了画面 (Ordersコントローラ)
サンプルプログラムのソースコードはこちらを参照してください。
画面イメージ
DB
DBは以下の3つのテーブルで構成されています。
- ユーザ(users)
- メニュー(menus)
- 注文(orders)
RSpecの使い方について
その1: テストコードの作成
テストコードは、specディレクトリ内に、specファイルを作成して記述します。ログイン処理のテストであれば、ログイン処理を実装しているSessionsコントローラに関連するものとして、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ファイル全体では以下のようになります。
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_url
や new_session_path
で取得することができます。これらのヘルパーメソッドでURL部分を置き換えてみましょう。
Hint 3
headless chromeの画面を直接見ることはできませんが、必要なタイミングでスクリーンショットを作成することができます。スクリーンショットを作成するには以下のように記述します。
take_screenshot
ログイン画面にアクセスした後に、スクリーンショットを作成してみましょう。テストコード内、visit
メソッド呼び出しの次の行に追加して、実行してみてください。
スクリーンショットは tmp/screenshots/
というディレクトリに、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_in
、click_button
で行うことができます。
Hint 3
ログインに成功すると、メニュー一覧画面が表示されます。ログインに成功したかどうかは、スクリーンショットを作成する、または、ページの内容にMenus
という文字が含まれているかどうかを判定する、などの方法により確認することができます。
課題3
注文画面を完成させてください。
説明
ユーザがWebサイトにログインし、メニュー画面でいずれかのメニューを選択したとします。すると、注文画面が表示されます。
この注文画面のビューとコントローラの開発を行ってください。
注文画面には以下の入力項目があるとします。これらの項目がある入力フォームをビュー(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 | テキスト |
params のorder 内、delivery_address
|
支払方法 | payment_method | ドロップダウン |
params のorder 内、payment_method
|
カード番号 | card_number | テキスト |
params のorder 内、card_number
|
Ordersコントローラのorder_params
メソッドに、上記の属性値を取り出すコードを記述してください。
テストコードは、spec/system/orders_spec.rb
に記述してください。
Hint 1
ビューファイル _form.html.erb
内での、テキスト入力欄、送信ボタンなどの記述方法は、menus
やusers
などのビューファイルを参考にしてください。
ドロップダウンの記述方法は、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の解答例
require 'rails_helper'
RSpec.describe "Sessions", type: :system, js: true do
it 'ログイン画面が表示されること' do
# ここにテストコードを書く
visit new_session_path
take_screenshot
end
end
課題2の解答例
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
を使って書く場合は以下のようになります。
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の解答例
ビュー
<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>
コントローラ
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
テストコード
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でしたが、他の開発言語、フレームワークでも同様なことができるものもあるでしょう。
テストコードはテストを行う手段というだけでなく、開発時にも使ってみていただけるとよいと思います。