はじめに
第11回目ですね。
前回はテストを自動化するためにRSpec、Selenium、Capybaraなどを導入しましたね。
今日は今まで作ってきたアプリケーションに対してテストコードをコーディングしていきます。
本当はアプリをコーディングする前にテストをコーディングしてRedのフェーズにするべきなのですが、
まぁ最初なのでご愛嬌ということでGreenの状態から始めましょう。
前回のソースコード
前回のソースコードはこちらに格納してます。今回のハンズオンからやりたい場合はこちらからダウンロードしてください。
どういうふうにテストコード書いてくの?
ここは人によってやりやすいようにでいいと思うのですが、このハンズオンでは基本的には作りたい機能(ユーザーストーリー)ごとにテストファイルを分けて記述していきます。
例えば、今までだと「サインアップ」とか「サインイン」とかそういうやつです。
例えば以下のようにテストシナリオを考えてみます。
1. ユーザーとして、ページにダイレクトアクセスしたい
- 未サインインのユーザーが、トップページにダイレクトアクセスしたとき、トップページが表示されること
- 未サインインのユーザーが、サインアップページにダイレクトアクセスしたとき、サインアップページが表示されること
- 未サインインのユーザーが、サインインページにダイレクトアクセスしたとき、サインインページが表示されること
- 未サインインのユーザーが、ユーザー詳細ページにダイレクトアクセスしたとき、ユーザー詳細ページが表示されること
- サインイン済のユーザーが、トップページにダイレクトアクセスしたとき、そのユーザーのユーザー詳細ページが表示されること
- サインイン済のユーザーが、サインアップページにダイレクトアクセスしたとき、そのユーザーのユーザー詳細ページが表示されること
- サインイン済のユーザーが、サインインページにダイレクトアクセスしたとき、そのユーザーのユーザー詳細ページが表示されること
- サインイン済のユーザーが、ユーザー詳細ページにダイレクトアクセスしたとき、ユーザー詳細ページが表示されること
2. ユーザーとして、ヘッダーリンクからページ遷移できること
- 未サインインのユーザーが、トップページでヘッダーのロゴをクリックしたとき、トップページに遷移すること
- 未サインインのユーザーが、トップページでヘッダーの「Home」リンクをクリックしたとき、トップページに遷移すること
- 未サインインのユーザーが、トップページでヘッダーの「Sign in」リンクをクリックしたとき、サインインページに遷移すること
- 未サインインのユーザーは、トップページでヘッダーに「Profile」リンクが存在しないこと
- 未サインインのユーザーは、トップページでヘッダーに「Sign out」リンクが存在しないこと
- 未サインインのユーザーが、サインアップページでヘッダーのロゴをクリックしたとき、トップページに遷移すること
- 未サインインのユーザーが、サインアップページでヘッダーの「Home」リンクをクリックしたとき、トップページに遷移すること
- 未サインインのユーザーが、サインアップページでヘッダーの「Sign in」リンクをクリックしたとき、サインインページに遷移すること
- 未サインインのユーザーは、サインアップページでヘッダーに「Profile」リンクが存在しないこと
- 未サインインのユーザーは、サインアップページでヘッダーに「Sign out」リンクが存在しないこと
- 未サインインのユーザーが、サインインページでヘッダーのロゴをクリックしたとき、トップページに遷移すること
- 未サインインのユーザーが、サインインページでヘッダーの「Home」リンクをクリックしたとき、トップページに遷移すること
- 未サインインのユーザーが、サインインページでヘッダーの「Sign in」リンクをクリックしたとき、サインインページに遷移すること
- 未サインインのユーザーは、サインインページでヘッダーに「Profile」リンクが存在しないこと
- 未サインインのユーザーは、サインインページでヘッダーに「Sign out」リンクが存在しないこと
- 未サインインのユーザーが、ユーザー詳細ページでヘッダーのロゴをクリックしたとき、トップページに遷移すること
- 未サインインのユーザーが、ユーザー詳細ページでヘッダーの「Home」リンクをクリックしたとき、トップページに遷移すること
- 未サインインのユーザーが、ユーザー詳細ページでヘッダーの「Sign in」リンクをクリックしたとき、サインインページに遷移すること
- 未サインインのユーザーは、ユーザー詳細ページでヘッダーに「Profile」リンクが存在しないこと
- 未サインインのユーザーは、ユーザー詳細ページでヘッダーに「Sign out」リンクが存在しないこと
- サインイン済のユーザーが、ユーザー詳細ページでヘッダーのロゴをクリックしたとき、そのユーザーのユーザー詳細ページに遷移すること
- サインイン済のユーザーは、ユーザー詳細ページでヘッダーに「Home」リンクが存在しないこと
- サインイン済のユーザーは、ユーザー詳細ページでヘッダーに「Sign in」リンクが存在しないこと
- サインイン済のユーザーが、ユーザー詳細ページでヘッダーの「Profile」リンクをクリックしたとき、そのユーザーのユーザー詳細ページに遷移すること
- サインイン済のユーザーが、ユーザー詳細ページでヘッダーの「Sign out」リンクが存在すること
3. ユーザーとして、サインアップしたい
- 未サインインのユーザーが、トップページで「Sign up now!」ボタンを選択したとき、サインアップページに遷移すること
- サインアップページで「お名前」を入力できること
- サインアップページで「メールアドレス」を入力できること
- サインアップページで「パスワード」を入力できること
- サインアップページで「パスワード」はマスク化されること
- サインアップページで「パスワード」を入力したユーザーが、「パスワードを表示する」チェックボックスにチェックをいれたとき、「パスワード」が表示されること
- サインアップページで「パスワード」を入力したユーザーが、「パスワードを表示する」チェックボックスのチェックを外したとき、「パスワード」がマスク化されること
- サインアップページで「お名前」を入力していないユーザーが、「Sign up!」ボタンをクリックしたとき、サインアップは失敗し「お名前」未入力のエラーメッセージが表示されること
- サインアップページで「お名前」を51文字以上入力したユーザーが、「Sign up!」ボタンをクリックしたとき、サインアップは失敗し「お名前」文字数超過のエラーメッセージが表示されること
- サインアップページで「メールアドレス」を入力していないユーザーが、「Sign up!」ボタンをクリックしたとき、サインアップは失敗し「メールアドレス」未入力のエラーメッセージが表示されること
- サインアップページで「メールアドレス」を256文字以上入力したユーザーが、「Sign up!」ボタンをクリックしたとき、サインアップは失敗し「メールアドレス」文字数超過のエラーメッセージが表示されること
- サインアップページで「メールアドレス」を誤ったフォーマットで入力したユーザーが、「Sign up!」ボタンをクリックしたとき、サインアップは失敗し「メールアドレス」フォーマットチェックエラーのエラーメッセージが表示されること
- サインアップページで「メールアドレス」がすでに登録済みのメールアドレスを入力したユーザーが、「Sign up!」ボタンをクリックしたとき、サインアップは失敗し「メールアドレス」重複のエラーメッセージが表示されること
- サインアップページで「パスワード」を入力していないユーザーが、「Sign up!」ボタンをクリックしたとき、サインアップは失敗し「パスワード」文字数不足のエラーメッセージが表示されること
- サインアップページで「パスワード」を5文字以下で入力したユーザーが、「Sign up!」ボタンをクリックしたとき、サインアップは失敗し「パスワード」文字数不足のエラーメッセージが表示されること
- サインアップページで「お名前」「メールアドレス」「パスワード」を正しく入力したユーザーが、「Sign up!」ボタンをクリックしたとき、サインアップは成功し、そのユーザーのユーザー詳細ページにサインイン済状態で遷移すること
- サインアップに成功したユーザーは、遷移後のユーザー詳細ページで自分の入力した「お名前」を確認できること
- サインアップに成功したユーザーは、遷移後のユーザー詳細ページで自分の入力した「メールアドレス」を確認できること
- サインアップに成功したユーザーは、遷移後のユーザー詳細ページでウェルカムメッセージを確認できること
- サインアップに成功したユーザーは、遷移後のユーザー詳細ページをリロードしたとき、ウェルカムメッセージを確認できなくなること
- サインアップページで「登録済みの方はこちら」リンクを選択したとき、サインインページに遷移すること
4. ユーザーとして、サインインしたい
- サインインページで「メールアドレス」を入力できること
- サインインページで「パスワード」を入力できること
- サインインページで「パスワード」はマスク化されること
- サインインページで「パスワード」を入力したユーザーが、「パスワードを表示する」チェックボックスにチェックをいれたとき、「パスワード」が表示されること
- サインインページで「パスワード」を入力したユーザーが、「パスワードを表示する」チェックボックスのチェックを外したとき、「パスワード」がマスク化されること
- サインインページで「メールアドレス」を入力していないユーザーが、「Sign in」ボタンをクリックしたとき、サインイン失敗のエラーメッセージが表示されること
- サインインページで「メールアドレス」として登録されていないメールアドレスを入力したユーザーが、「Sign in」ボタンをクリックしたとき、サインイン失敗のエラーメッセージが表示されること
- サインインページで「メールアドレス」は正しいが「パスワード」を入力していないユーザーが、「Sign in」ボタンをクリックしたとき、サインイン失敗のエラーメッセージが表示されること
- サインインページで「メールアドレス」は正しいが「パスワード」が正しくないユーザーが、「Sign in」ボタンをクリックしたとき、サインイン失敗のエラーメッセージが表示されること
- サインインページで「メールアドレス」「パスワード」に正しい値を入力したユーザーが、「Sign in」ボタンをクリックしたとき、サインイン済状態でそのユーザーのユーザー詳細ページに遷移すること
- サインインに成功したユーザーは、遷移後のユーザー詳細ページでサインイン成功メッセージを確認できること
- サインインに成功したユーザーは、遷移後のユーザー詳細ページをリロードしたとき、サインイン成功メッセージを確認できなくなること
- サインインページで「登録がまだの方はこちら」リンクを選択したとき、サインアップページに遷移すること
5. ユーザーとして、サインアウトしたい
- サインイン済のユーザーが、ユーザー詳細ページでヘッダーの「Sign out」リンクをクリックしたとき、未サインイン状態になりトップページに遷移すること
6. ユーザーとして、他のユーザーの情報を閲覧したい
- ユーザーが、存在するユーザーのユーザー詳細ページにアクセスしようとしたとき、そのユーザーの「お名前」「メールアドレス」を確認できること
- ユーザーが、存在しないユーザーのユーザー詳細ページにアクセスしようとしたとき、エラーが発生すること
ざっとあげただけでもこれだけのテストシナリオがあります。
こんなにコード書かなきゃいけないのかよ!と思うかもしれませんが、コードを書かないと少しのリファクタリングの度にこれら全てのテストを手動で行わなければ安心してデプロイできないという修羅の道を選ぶことになります。
今日でテストコードへのハードルを爆下げして気軽にリファクタできるエンジニアをめざしましょう!
テストコードを書いていこう
ここから実際にテストコードを書いていきます。
上でナンバリングで章立てしてましたね。それごとにスペックファイルを作って管理します。
1. ユーザーとして、ページにダイレクトアクセスしたい
ファイル名は01_direct_access_spec.rb
にしておきましょう。
$ mkdir spec/system/
$ touch spec/system/01_direct_access_spec.rb
feature "ユーザーとして、ページにダイレクトアクセスしたい", type: :system do
background do
@user1 = User.create(name: "John Smith", email: "john@sample.com", password: "john1234")
@user2 = User.create(name: "Taro Tanaka", email: "taro@sample.com", password: "taro1234")
end
scenario "未サインインのユーザーが、トップページにダイレクトアクセスしたとき、トップページが表示されること" do
visit root_path
expect(current_path).to eq root_path
end
scenario "未サインインのユーザーが、サインアップページにダイレクトアクセスしたとき、サインアップページが表示されること" do
visit sign_up_path
expect(current_path).to eq sign_up_path
end
scenario "未サインインのユーザーが、サインインページにダイレクトアクセスしたとき、サインインページが表示されること" do
visit sign_in_path
expect(current_path).to eq sign_in_path
end
scenario "未サインインのユーザーが、ユーザー詳細ページにダイレクトアクセスしたとき、ユーザー詳細ページが表示されること" do
visit user_path(@user1)
expect(current_path).to eq user_path(@user1)
visit user_path(@user2)
expect(current_path).to eq user_path(@user2)
end
feature nil, type: :system do
background do
visit sign_in_path
fill_in :user_email, with: @user1.email
fill_in :user_password, with: @user1.password
click_on :sign_in_button
end
scenario "サインイン済のユーザーが、トップページにダイレクトアクセスしたとき、そのユーザーのユーザー詳細ページが表示されること" do
visit root_path
expect(current_path).to eq user_path(@user1)
end
scenario "サインイン済のユーザーが、サインアップページにダイレクトアクセスしたとき、そのユーザーのユーザー詳細ページが表示されること" do
visit sign_up_path
expect(current_path).to eq user_path(@user1)
end
scenario "サインイン済のユーザーが、サインインページにダイレクトアクセスしたとき、そのユーザーのユーザー詳細ページが表示されること" do
visit sign_in_path
expect(current_path).to eq user_path(@user1)
end
scenario "サインイン済のユーザーが、ユーザー詳細ページにダイレクトアクセスしたとき、ユーザー詳細ページが表示されること" do
visit user_path(@user1)
expect(current_path).to eq user_path(@user1)
visit user_path(@user2)
expect(current_path).to eq user_path(@user2)
end
end
end
このテストをパスさせるために、サインインページに少し細工をします。
- <%= form.submit "Sign in", class: "btn btn-primary form-control" %>
+ <%= form.submit "Sign in", class: "btn btn-primary form-control", id: :sign_in_button %>
id: :sign_in_button
を追記しました。これでid
属性を追加できます。
まずはテストがパスするのを体感しましょうか!
$ docker-compose up -d
$ docker-compose exec web ash
# rspec spec/system/01_direct_access_spec.rb
Capybara starting Puma...
* Version 4.3.1 , codename: Mysterious Traveller
* Min threads: 0, max threads: 4
* Listening on tcp://127.0.0.1:45497
........
Finished in 11.85 seconds (files took 5.4 seconds to load)
8 examples, 0 failures
テストパスしましたね!
どんなテストが実行されたのか、ちょっと紹介させてください!
構文
前回も紹介しましたが、RSpecのシステムテストの構文は
feature "test name", type: :system do
scenario "test scenario" do
# テストコード
end
end
です。scenario
は複数あります。
feature
内の全てのscenario
に適用する初期条件を記述する場合はbackground
を使います。
feature "test name", type: :system do
background do
# 前提条件
end
scenario "test scenario" do
# テストコード
end
end
また、feature
を入れ子にすることも可能です。これによって特定のscenario
たちにだけ前提条件をつけることも可能です。
feature "test name", type: :system do
scenario "test scenario" do
# 前提条件が適用されない
end
feature "test detail name", type: :system do
background do
# 前提条件
end
scenario "test scenario" do
# 前提条件が適用される
end
end
end
まずはこの構文を身に付けましょう。
background
background
は前提条件を定義するためのブロックです。そのファイルのシナリオに共通して行われる処理をここで定義します。
よく使われる場面としては、今回のようにモデルを作っておく、とかですね。
モデルは今までのRubyコードと同じように、User.create
やUser.new
が使えます。また、インスタンス変数にしないとscenario
側では参照できないので注意してくださいね。
visit
visit
は引数に名前付きルート(xxxx_path)やURLをとって、そこにアクセスします。
visit root_path
これでroot_path
、つまり/
にアクセスをしようとします。
expect().to
expect().to
は()
内をto
以降と検証します。
例えばexpect().to eq xxxxx
のようにeq
と組み合わせることで()
内とxxxxx
が等しいことを検証します。
この検証がfalse
の場合はそのシナリオをFailureになります。
current_path
current_path
は現在表示されているパス(/
とか/users/1
とか)を取得します。
expect(current_path).to eq root_path
で、今表示されているページのパスがルートパス、つまりトップページであるかどうかを検証しているのです。
fill_in
fill_in
はinput
に文字を入力する操作を実行します。
fill_in [id], with: [入力したい文字列]
で、ページからid
属性が[id]
のinput
に[入力したい文字列]
を入力してくれます。
click_on
click_on
は<a>
または<button>
タグをクリックする操作を実行します。
click_on [id]
で、ページからid
属性が[id]
の<a>
か<button>
タグをクリックしてくれます。
ここまでを理解すると
background do
visit sign_in_path
fill_in :user_email, with: @user1.email
fill_in :user_password, with: @user1.password
click_on :sign_in_button
end
が、サインインページにアクセスして@user1
でサインインしようとしていることがわかりますね?
こんな感じで操作と検証を組み合わせてテストを自動化していきます!
ではどんどんテストコードを書いていきましょう!!
2. ユーザーとして、ヘッダーリンクからページ遷移できること
ファイル名は02_header_spec.rb
でいきましょうか。
# touch spec/system/02_header_spec.rb
また少しid
を仕込んでおきましょう。
<div class="container">
- <%= link_to "sample app", root_path, class: "navbar-brand" %>
+ <%= link_to "sample app", root_path, class: "navbar-brand", id: :header_logo %>
<ul class="navbar-nav">
<% if signed_in? %>
<%# サインイン済みの場合のリンク %>
- <li class="nav-item"><%= link_to "Profile", current_user, class: "nav-link" %></li>
- <li class="nav-item"><%= link_to "Sign out", sign_out_path, method: :delete, class: "nav-link" %></li>
+ <li class="nav-item"><%= link_to "Profile", current_user, class: "nav-link", id: :header_profile_link %></li>
+ <li class="nav-item"><%= link_to "Sign out", sign_out_path, method: :delete, class: "nav-link", id: :header_sign_out_link %></li>
<% else %>
<%# 未サインインの場合のリンク %>
- <li class="nav-item"><%= link_to "Home", root_path, class: "nav-link" %></li>
- <li class="nav-item"><%= link_to "Sign in", sign_in_path, class: "nav-link" %></li>
+ <li class="nav-item"><%= link_to "Home", root_path, class: "nav-link", id: :header_home_link %></li>
+ <li class="nav-item"><%= link_to "Sign in", sign_in_path, class: "nav-link", id: :header_sign_in_link %></li>
<% end %>
</ul>
</div>
そしてテストシナリオを書きます。
feature "ユーザーとして、ヘッダーリンクからページ遷移できること", type: :system do
background do
@user1 = User.create(name: "John Smith", email: "john@sample.com", password: "john1234")
@user2 = User.create(name: "Taro Tanaka", email: "taro@sample.com", password: "taro1234")
end
scenario "未サインインのユーザーが、トップページでヘッダーのロゴをクリックしたとき、トップページに遷移すること" do
visit root_path
click_on :header_logo
expect(current_path).to eq root_path
end
scenario "未サインインのユーザーが、トップページでヘッダーの「Home」リンクをクリックしたとき、トップページに遷移すること" do
visit root_path
click_on :header_home_link
expect(current_path).to eq root_path
end
scenario "未サインインのユーザーが、トップページでヘッダーの「Sign in」リンクをクリックしたとき、サインインページに遷移すること" do
visit root_path
click_on :header_sign_in_link
expect(current_path).to eq sign_in_path
end
scenario "未サインインのユーザーは、トップページでヘッダーに「Profile」リンクが存在しないこと" do
visit root_path
expect(page).not_to have_selector "#header_profile_link"
end
scenario "未サインインのユーザーは、トップページでヘッダーに「Sign out」リンクが存在しないこと" do
visit root_path
expect(page).not_to have_selector "#header_sign_out_link"
end
scenario "未サインインのユーザーが、サインアップページでヘッダーのロゴをクリックしたとき、トップページに遷移すること" do
visit sign_up_path
click_on :header_logo
expect(current_path).to eq root_path
end
scenario "未サインインのユーザーが、サインアップページでヘッダーの「Home」リンクをクリックしたとき、トップページに遷移すること" do
visit sign_up_path
click_on :header_home_link
expect(current_path).to eq root_path
end
scenario "未サインインのユーザーが、サインアップページでヘッダーの「Sign in」リンクをクリックしたとき、サインインページに遷移すること" do
visit sign_up_path
click_on :header_sign_in_link
expect(current_path).to eq sign_in_path
end
scenario "未サインインのユーザーは、サインアップページでヘッダーに「Profile」リンクが存在しないこと" do
visit sign_up_path
expect(page).not_to have_selector "#header_profile_link"
end
scenario "未サインインのユーザーは、サインアップページでヘッダーに「Sign out」リンクが存在しないこと" do
visit sign_up_path
expect(page).not_to have_selector "#header_sign_out_path"
end
scenario "未サインインのユーザーが、サインインページでヘッダーのロゴをクリックしたとき、トップページに遷移すること" do
visit sign_in_path
click_on :header_logo
expect(current_path).to eq root_path
end
scenario "未サインインのユーザーが、サインインページでヘッダーの「Home」リンクをクリックしたとき、トップページに遷移すること" do
visit sign_in_path
click_on :header_home_link
expect(current_path).to eq root_path
end
scenario "未サインインのユーザーが、サインインページでヘッダーの「Sign in」リンクをクリックしたとき、サインインページに遷移すること" do
visit sign_in_path
click_on :header_sign_in_link
expect(current_path).to eq sign_in_path
end
scenario "未サインインのユーザーは、サインインページでヘッダーに「Profile」リンクが存在しないこと" do
visit sign_in_path
expect(page).not_to have_selector "#header_profile_path"
end
scenario "未サインインのユーザーは、サインインページでヘッダーに「Sign out」リンクが存在しないこと" do
visit sign_in_path
expect(page).not_to have_selector "#header_sign_out_path"
end
scenario "未サインインのユーザーが、ユーザー詳細ページでヘッダーのロゴをクリックしたとき、トップページに遷移すること" do
visit user_path(@user1)
click_on :header_logo
expect(current_path).to eq root_path
end
scenario "未サインインのユーザーが、ユーザー詳細ページでヘッダーの「Home」リンクをクリックしたとき、トップページに遷移すること" do
visit user_path(@user1)
click_on :header_home_link
expect(current_path).to eq root_path
end
scenario "未サインインのユーザーが、ユーザー詳細ページでヘッダーの「Sign in」リンクをクリックしたとき、サインインページに遷移すること" do
visit user_path(@user1)
click_on :header_sign_in_link
expect(current_path).to eq sign_in_path
end
scenario "未サインインのユーザーは、ユーザー詳細ページでヘッダーに「Profile」リンクが存在しないこと" do
visit user_path(@user1)
expect(page).not_to have_selector "#header_profile_link"
end
scenario "未サインインのユーザーは、ユーザー詳細ページでヘッダーに「Sign out」リンクが存在しないこと" do
visit user_path(@user1)
expect(page).not_to have_selector "#header_sign_out_link"
end
feature nil, type: :system do
background do
visit sign_in_path
fill_in :user_email, with: @user1.email
fill_in :user_password, with: @user1.password
click_on :sign_in_button
end
scenario "サインイン済のユーザーが、ユーザー詳細ページでヘッダーのロゴをクリックしたとき、そのユーザーのユーザー詳細ページに遷移すること" do
visit user_path(@user2)
click_on :header_logo
sleep 1
expect(current_path).to eq user_path(@user1)
end
scenario "サインイン済のユーザーは、ユーザー詳細ページでヘッダーに「Home」リンクが存在しないこと" do
visit user_path(@user2)
expect(page).not_to have_selector "#header_home_link"
end
scenario "サインイン済のユーザーは、ユーザー詳細ページでヘッダーに「Sign in」リンクが存在しないこと" do
visit user_path(@user2)
expect(page).not_to have_selector "#header_sign_in_link"
end
scenario "サインイン済のユーザーが、ユーザー詳細ページでヘッダーの「Profile」リンクをクリックしたとき、そのユーザーのユーザー詳細ページに遷移すること" do
visit user_path(@user2)
click_on :header_profile_link
expect(current_path).to eq user_path(@user1)
end
scenario "サインイン済のユーザーが、ユーザー詳細ページでヘッダーの「Sign out」リンクが存在すること" do
visit user_path(@user2)
expect(page).to have_selector "#header_sign_out_link"
end
end
end
ダイレクトアクセスのテストと似ているところが多いですが、新出のコードを紹介していきます!
page
expect(page)
という形で現れましたね。これは今表示されているページ全体のことです。
not_to
expect().not_to
という表現がでてきましたね。
to
の反対で()
とnot_to
以降がアンマッチであることを検証します。
マッチした場合にfalse
になり、そのシナリオがFailureになります。
have_selector
have_selector
は指定したタグや属性を持っているかを検証します。
例えば、以下のような要素があるとします。
<h1 id="title" class="main-title">Title</h1>
これをタグ、id
属性、class
属性でそれぞれhave_selector
で検証しようとすると以下のようになります。
# タグで検証
expect(page).to have_selector("h1")
# id属性で検証
expect(page).to have_selector("#title")
# class属性で検証
expect(page).to have_selector(".main-title")
ページ内や子要素に特定の要素がないかを調べる時に使うので覚えておくとよしです!
sleep
sleep
は指定した秒数、次のコードの実行を待つコードです。sleep 1
であれば1秒待った後に次の行に進みます。
今回は、アプリケーション側でサインイン状態を確認した後リダイレクトする処理を入れていますが、自動化されたテストがそのままのスピードで検証を進めてしまうとリダイレクト処理が終わる前に検証が完了してしまい、思ったとおりの結果を得られないことがあります。
こういった自体を防ぐために、sleep
を挟むことで処理を待たせることも必要になります。
ただし、sleep
の使用は最低限にするべきです。なぜならそのせいでテストの実行時間が長くなってしまっては自動化した意味が失われかねないからです。
3. ユーザーとして、サインアップしたい
ファイル名は03_sign_up_spec.rb
でいきます!
# touch spec/system/03_sign_up_spec.rb
そして、今回もid
を仕込みます。
- <%= link_to "Sign up now!", sign_up_path, class: "btn btn-lg btn-primary mt-5" %>
+ <%= link_to "Sign up now!", sign_up_path, class: "btn btn-lg btn-primary mt-5", id: :sign_up_link %>
<%= form_with model: @user, url: create_user_path, local: true do |form| %>
...
<div class="form-group mt-5">
- <%= form.submit "Sign up!", class: "form-control btn btn-primary" %>
+ <%= form.submit "Sign up!", class: "form-control btn btn-primary", id: :sign_up_button %>
</div>
<% end %>
- <p class="text-center">登録済みの方は<%= link_to "こちら", sign_in_path %></p>
+ <p class="text-center">登録済みの方は<%= link_to "こちら", sign_in_path, id: :sign_in_link %></p>
はい。ではテストコードです。
feature "ユーザーとして、サインアップしたい", type: :system do
background do
@user = User.new(name: "John Smith", email: "john@sample.com", password: "john1234")
end
scenario "未サインインのユーザーが、トップページで「Sign up now!」ボタンを選択したとき、サインアップページに遷移すること" do
visit root_path
click_on :sign_up_link
expect(current_path).to eq sign_up_path
end
scenario "サインアップページで「お名前」を入力できること" do
visit sign_up_path
fill_in :user_name, with: @user.name
expect(find("#user_name").value).to eq @user.name
end
scenario "サインアップページで「メールアドレス」を入力できること" do
visit sign_up_path
fill_in :user_email, with: @user.email
expect(find("#user_email").value).to eq @user.email
end
scenario "サインアップページで「パスワード」を入力できること" do
visit sign_up_path
fill_in :user_password, with: @user.password
expect(find("#user_password").value).to eq @user.password
end
scenario "サインアップページで「パスワード」はマスク化されること" do
visit sign_up_path
fill_in :user_password, with: @user.password
expect(find("#user_password")[:type]).to eq "password"
end
scenario "サインアップページで「パスワード」を入力したユーザーが、「パスワードを表示する」チェックボックスにチェックをいれたとき、「パスワード」が表示されること" do
visit sign_up_path
fill_in :user_password, with: @user.password
check :visible_password
expect(find("#user_password")[:type]).to eq "text"
end
scenario "サインアップページで「パスワード」を入力したユーザーが、「パスワードを表示する」チェックボックスのチェックを外したとき、「パスワード」がマスク化されること" do
visit sign_up_path
fill_in :user_password, with: @user.password
check :visible_password
uncheck :visible_password
expect(find("#user_password")[:type]).to eq "password"
end
scenario "サインアップページで「お名前」を入力していないユーザーが、「Sign up!」ボタンをクリックしたとき、サインアップは失敗し「お名前」未入力のエラーメッセージが表示されること" do
error_message = "お名前を入力してください"
visit sign_up_path
fill_in :user_name, with: ""
fill_in :user_email, with: @user.email
fill_in :user_password, with: @user.password
click_on :sign_up_button
expect(current_path).to eq sign_up_path
expect(page).to have_text error_message
end
scenario "サインアップページで「お名前」を51文字以上入力したユーザーが、「Sign up!」ボタンをクリックしたとき、サインアップは失敗し「お名前」文字数超過のエラーメッセージが表示されること" do
error_message = "お名前は50文字以内で入力してください"
visit sign_up_path
fill_in :user_name, with: "a" * 51
fill_in :user_email, with: @user.email
fill_in :user_password, with: @user.password
click_on :sign_up_button
expect(current_path).to eq sign_up_path
expect(page).to have_text error_message
fill_in :user_name, with: "a" * 50
fill_in :user_password, with: @user.password
click_on :sign_up_button
expect(current_path).not_to eq sign_up_path
expect(page).not_to have_text error_message
expect(current_path).to eq user_path(User.find_by(email: @user.email))
end
scenario "サインアップページで「メールアドレス」を入力していないユーザーが、「Sign up!」ボタンをクリックしたとき、サインアップは失敗し「メールアドレス」未入力のエラーメッセージが表示されること" do
error_message = "メールアドレスを入力してください"
visit sign_up_path
fill_in :user_name, with: @user.name
fill_in :user_email, with: ""
fill_in :user_password, with: @user.password
click_on :sign_up_button
expect(current_path).to eq sign_up_path
expect(page).to have_text error_message
end
scenario "サインアップページで「メールアドレス」を256文字以上入力したユーザーが、「Sign up!」ボタンをクリックしたとき、サインアップは失敗し「メールアドレス」文字数超過のエラーメッセージが表示されること" do
error_message = "メールアドレスは255文字以内で入力してください"
visit sign_up_path
fill_in :user_name, with: @user.name
fill_in :user_email, with: "a" * 245 + "@sample.com"
fill_in :user_password, with: @user.password
click_on :sign_up_button
expect(current_path).to eq sign_up_path
expect(page).to have_text error_message
fill_in :user_email, with: "a" * 244 + "@sample.com"
fill_in :user_password, with: @user.password
click_on :sign_up_button
expect(current_path).not_to eq sign_up_path
expect(page).not_to have_text error_message
expect(current_path).to eq user_path(User.find_by(email: "a" * 244 + "@sample.com"))
end
scenario "サインアップページで「メールアドレス」を誤ったフォーマットで入力したユーザーが、「Sign up!」ボタンをクリックしたとき、サインアップは失敗し「メールアドレス」フォーマットチェックエラーのエラーメッセージが表示されること" do
error_message = "メールアドレスは不正な値です"
visit sign_up_path
fill_in :user_name, with: @user.name
fill_in :user_email, with: "sample.com"
fill_in :user_password, with: @user.password
click_on :sign_up_button
expect(current_path).to eq sign_up_path
expect(page).to have_text error_message
end
scenario "サインアップページで「メールアドレス」がすでに登録済みのメールアドレスを入力したユーザーが、「Sign up!」ボタンをクリックしたとき、サインアップは失敗し「メールアドレス」重複のエラーメッセージが表示されること" do
error_message = "メールアドレスはすでに存在します"
@user.save
visit sign_up_path
fill_in :user_name, with: @user.name
fill_in :user_email, with: @user.email.upcase
fill_in :user_password, with: @user.password
click_on :sign_up_button
expect(current_path).to eq sign_up_path
expect(page).to have_text error_message
end
scenario "サインアップページで「パスワード」を入力していないユーザーが、「Sign up!」ボタンをクリックしたとき、サインアップは失敗し「パスワード」文字数不足のエラーメッセージが表示されること" do
error_message = "パスワードは6文字以上で入力してください"
visit sign_up_path
fill_in :user_name, with: @user.name
fill_in :user_email, with: @user.email
fill_in :user_password, with: ""
click_on :sign_up_button
expect(current_path).to eq sign_up_path
expect(page).to have_text error_message
end
scenario "サインアップページで「パスワード」を5文字以下で入力したユーザーが、「Sign up!」ボタンをクリックしたとき、サインアップは失敗し「パスワード」文字数不足のエラーメッセージが表示されること" do
error_message = "パスワードは6文字以上で入力してください"
visit sign_up_path
fill_in :user_name, with: @user.name
fill_in :user_email, with: @user.email
fill_in :user_password, with: "john1"
click_on :sign_up_button
expect(current_path).to eq sign_up_path
expect(page).to have_text error_message
fill_in :user_password, with: "john12"
click_on :sign_up_button
expect(current_path).not_to eq sign_up_path
expect(page).not_to have_text error_message
expect(current_path).to eq user_path(User.find_by(email: @user.email))
end
feature nil, type: :system do
background do
@welcome_message = "サインアップありがとう"
visit sign_up_path
fill_in :user_name, with: @user.name
fill_in :user_email, with: @user.email
fill_in :user_password, with: @user.password
click_on :sign_up_button
end
scenario "サインアップページで「お名前」「メールアドレス」「パスワード」を正しく入力したユーザーが、「Sign up!」ボタンをクリックしたとき、サインアップは成功し、そのユーザーのユーザー詳細ページにサインイン済状態で遷移すること" do
expect(current_path).to eq user_path(User.find_by(email: @user.email))
expect(page).not_to have_selector "#header_sign_in_link"
expect(page).to have_selector "#header_sign_out_link"
end
scenario "サインアップに成功したユーザーは、遷移後のユーザー詳細ページで自分の入力した「お名前」を確認できること" do
expect(page).to have_text @user.name
end
scenario "サインアップに成功したユーザーは、遷移後のユーザー詳細ページで自分の入力した「メールアドレス」を確認できること" do
expect(page).to have_text @user.email
end
scenario "サインアップに成功したユーザーは、遷移後のユーザー詳細ページでウェルカムメッセージを確認できること" do
expect(page).to have_text @welcome_message
end
scenario "サインアップに成功したユーザーは、遷移後のユーザー詳細ページをリロードしたとき、ウェルカムメッセージを確認できなくなること" do
visit current_path
expect(page).not_to have_text @welcome_message
end
end
scenario "サインアップページで「登録済みの方はこちら」リンクを選択したとき、サインインページに遷移すること" do
visit sign_up_path
click_on :sign_in_link
expect(current_path).to eq sign_in_path
end
end
また、はじめましての書き方を紹介していきます。
find
モデルのときにつかったfind
とはまた別ですよ。
find()
でページの中から()
内で指定した要素を取得してくれます。1つ以上該当するものがあるとエラーになってしまうので、id
属性に対して使うのが好ましいでしょう。
例えば今回のテストコードでは、
expect(find("#user_name").value).to eq @user.name
のように使っていますが、これでid
属性がuser_name
に定義されている要素を取得します。
value
find("#user_name").value
のようにvalue
を使っています。これはinput
にvalue
属性を取得しています。
value
属性にはinput type="text"
などの場合にはテキストボックスにデフォルトで入力しておきたい文字列を入力しておいたりしますが、Capybaraではvalue
属性を取得することで今入力されている文字列を取得することができます。
[:type]
find("#user_password")[:type]
のように使っています。Capybaraではvalue
とtext
は要素と.
でつなぐことで取得できるのですが、それ以外の属性は[:attribute_name]
の形式で取得します。[:type]
だとtype
属性を取得してきていることになりますね。
今回はpassword
のtype
属性をjavascriptでtext
とpassword
を切り替えているので、これでチェックができます。text
はマスク化なし、password
はマスク化ありはHTML5の仕様なので、今回のテストではtype
属性が正しく指定されているかを検証しました。
check
check
はチェックボックスにチェックする操作です。
今回は
check :visible_password
の形式でid
属性がvisible_password
のチェックボックスにチェックを入れています。
uncheck
uncheck
はcheck
の反対でチェックボックスからチェックを外す操作です。
have_text
have_text
は指定した文字列が存在するかどうかを検証するために使います。
expect(page).to have_text xxxxxxxxxx
と記述することでページのどこかにでもxxxxxxxxxx
の文字列が存在しないかを検証します。
page
の箇所をfind()
などで限定した要素にすることで、その要素内にxxxxxxxxxx
の文字列が存在するかどうかを検証するように範囲を狭めることもできます。
visit current_path
以前お話したようにcurrent_path
は現在のパスです。そこにvisit
しているということは...
そう!これはリロード操作ですね。
はい。今回のテストコードで新しく出てきた表現はこのくらいではないでしょうか。
そろそろ慣れてきましたか?一回書き始めると案外それらの組み合わせだけでいろいろなテストを実行できることがわかってきたんじゃないかと思います。
それでは次はサインインのテストコードを記述していきましょう!
4. ユーザーとして、サインインしたい
まずはシナリオファイルの作成から。
# touch spec/system/04_sign_in_spec.rb
そして、必要な箇所にid
属性を振ります。
- <p class="text-center">登録がまだの方は<%= link_to "こちら", sign_up_path %></p>
+ <p class="text-center">登録がまだの方は<%= link_to "こちら", sign_up_path, id: :sign_up_link %></p>
以下、テストコードです。今回は目新しい表現はないので、下のコードを見ずに書いてみてもらっても面白いかもしれないです。
feature "ユーザーとして、サインインしたい", type: :system do
background do
@user = User.create(name: "John Smith", email: "john@sample.com", password: "john1234")
end
scenario "サインインページで「メールアドレス」を入力できること" do
visit sign_in_path
fill_in :user_email, with: @user.email
expect(find("#user_email").value).to eq @user.email
end
scenario "サインインページで「パスワード」を入力できること" do
visit sign_in_path
fill_in :user_password, with: @user.password
expect(find("#user_password").value).to eq @user.password
end
scenario "サインインページで「パスワード」はマスク化されること" do
visit sign_in_path
fill_in :user_password, with: @user.password
expect(find("#user_password")[:type]).to eq "password"
end
scenario "サインインページで「パスワード」を入力したユーザーが、「パスワードを表示する」チェックボックスにチェックをいれたとき、「パスワード」が表示されること" do
visit sign_in_path
fill_in :user_password, with: @user.password
check :visible_password
expect(find("#user_password")[:type]).to eq "text"
end
scenario "サインインページで「パスワード」を入力したユーザーが、「パスワードを表示する」チェックボックスのチェックを外したとき、「パスワード」がマスク化されること" do
visit sign_in_path
fill_in :user_password, with: @user.password
check :visible_password
uncheck :visible_password
expect(find("#user_password")[:type]).to eq "password"
end
scenario "サインインページで「メールアドレス」を入力していないユーザーが、「Sign in」ボタンをクリックしたとき、サインイン失敗のエラーメッセージが表示されること" do
error_message = "メールアドレスまたはパスワードをもう一度確認してください。"
visit sign_in_path
fill_in :user_email, with: ""
fill_in :user_password, with: @user.password
click_on :sign_in_button
expect(current_path).to eq sign_in_path
expect(page).to have_text error_message
end
scenario "サインインページで「メールアドレス」として登録されていないメールアドレスを入力したユーザーが、「Sign in」ボタンをクリックしたとき、サインイン失敗のエラーメッセージが表示されること" do
error_message = "メールアドレスまたはパスワードをもう一度確認してください。"
visit sign_in_path
fill_in :user_email, with: "dummy@sample.com"
fill_in :user_password, with: @user.password
click_on :sign_in_button
expect(current_path).to eq sign_in_path
expect(page).to have_text error_message
end
scenario "サインインページで「メールアドレス」は正しいが「パスワード」を入力していないユーザーが、「Sign in」ボタンをクリックしたとき、サインイン失敗のエラーメッセージが表示されること" do
error_message = "メールアドレスまたはパスワードをもう一度確認してください。"
visit sign_in_path
fill_in :user_email, with: @user.email
fill_in :user_password, with: ""
click_on :sign_in_button
expect(current_path).to eq sign_in_path
expect(page).to have_text error_message
end
scenario "サインインページで「メールアドレス」は正しいが「パスワード」が正しくないユーザーが、「Sign in」ボタンをクリックしたとき、サインイン失敗のエラーメッセージが表示されること" do
error_message = "メールアドレスまたはパスワードをもう一度確認してください。"
visit sign_in_path
fill_in :user_email, with: @user.email
fill_in :user_password, with: @user.password + "a"
click_on :sign_in_button
expect(current_path).to eq sign_in_path
expect(page).to have_text error_message
end
feature nil, type: :system do
background do
@sign_in_message = "サインインしました。"
visit sign_in_path
fill_in :user_email, with: @user.email
fill_in :user_password, with: @user.password
click_on :sign_in_button
end
scenario "サインインページで「メールアドレス」「パスワード」に正しい値を入力したユーザーが、「Sign in」ボタンをクリックしたとき、サインイン済状態でそのユーザーのユーザー詳細ページに遷移すること" do
expect(current_path).to eq user_path(@user)
expect(page).not_to have_selector "#header_sign_in_link"
expect(page).to have_selector "#header_sign_out_link"
end
scenario "サインインに成功したユーザーは、遷移後のユーザー詳細ページでサインイン成功メッセージを確認できること" do
expect(page).to have_text @sign_in_message
end
scenario "サインインに成功したユーザーは、遷移後のユーザー詳細ページをリロードしたとき、サインイン成功メッセージを確認できなくなること" do
visit current_path
expect(page).not_to have_text @sign_in_message
end
end
scenario "サインインページで「登録がまだの方はこちら」リンクを選択したとき、サインアップページに遷移すること" do
visit sign_in_path
click_on :sign_up_link
expect(current_path).to eq sign_up_path
end
end
5. ユーザーとして、サインアウトしたい
次はサインアウトについてですね。
# touch spec/system/05_sign_out_spec.rb
そしてコーディング。これも今までのコードの組み合わせで表現可能ですね。
feature "ユーザーとして、サインアウトしたい", type: :system do
scenario "サインイン済のユーザーが、ユーザー詳細ページでヘッダーの「Sign out」リンクをクリックしたとき、未サインイン状態になりトップページに遷移すること" do
user = User.create(name: "John Smith", email: "john@sample.com", password: "john1234")
visit sign_in_path
fill_in :user_email, with: user.email
fill_in :user_password, with: user.password
click_on :sign_in_button
visit user_path(user)
click_on :header_sign_out_link
expect(current_path).to eq root_path
expect(page).to have_selector "#header_sign_in_link"
expect(page).not_to have_selector "#header_sign_out_link"
end
end
コメントアウトなどで説明文を書いたりは省いていますが、今までの内容が理解できていれば何をしているのか想像できると思います。
サインインページにアクセスしてサインインし、ユーザー詳細ページでヘッダーのサインアウトリンクをクリックし、トップページにリダイレクトされたと同時に未サインイン状態になっていることを検証していますね。
6. ユーザーとして、他のユーザーの情報を閲覧したい
まずはシナリオファイルです。
# touch spec/system/06_show_user_info_spec.rb
今回のテストでは、NotFoundのユーザーのユーザー詳細ページを表示しようとした時に、NotFoundのページが表示される、という項目があります。
Railsでは、NotFoundの例外が発生した場合、production
環境の場合はデフォルトでNotFound用のページが表示されるようになっています。
test
環境でも同じようにNotFoundページが表示されるようにconfigを変更します。
- config.consider_all_requests_local = true
+ config.consider_all_requests_local = false
- config.action_dispatch.show_exceptions = false
+ config.action_dispatch.show_exceptions = true
これで準備完了です。
NotFoundの場合、public/404.html
が表示されるようになります。
中身を見ると、
<h1>The page you were looking for doesn't exist.</h1>
と記述されているので、この文字列があるかどうかをチェックするようにします。
ではテストコードです。
feature "ユーザーとして、他のユーザーの情報を閲覧したい", type: :system do
background do
@user1 = User.create(name: "John Smith", email: "john@sample.com", password: "john1234")
@user2 = User.create(name: "Taro Yamada", email: "taro@sample.com", password: "taro1234")
end
scenario "ユーザーが、存在するユーザーのユーザー詳細ページにアクセスしようとしたとき、そのユーザーの「お名前」「メールアドレス」を確認できること" do
# Before sign in
visit user_path(@user1)
expect(page).to have_text @user1.name
expect(page).to have_text @user1.email
expect(page).not_to have_text @user2.name
expect(page).not_to have_text @user2.email
visit user_path(@user2)
expect(page).not_to have_text @user1.name
expect(page).not_to have_text @user1.email
expect(page).to have_text @user2.name
expect(page).to have_text @user2.email
# After sign in
visit sign_in_path
fill_in :user_email, with: @user1.email
fill_in :user_password, with: @user1.password
click_on :sign_in_button
visit user_path(@user1)
expect(page).to have_text @user1.name
expect(page).to have_text @user1.email
expect(page).not_to have_text @user2.name
expect(page).not_to have_text @user2.email
visit user_path(@user2)
expect(page).not_to have_text @user1.name
expect(page).not_to have_text @user1.email
expect(page).to have_text @user2.name
expect(page).to have_text @user2.email
end
scenario "ユーザーが、存在しないユーザーのユーザー詳細ページにアクセスしようとしたとき、エラーが発生すること" do
not_found_message = "The page you were looking for doesn't exist."
not_found_id = @user2.id + 1
expect{User.find(not_found_id)}.to raise_exception(ActiveRecord::RecordNotFound)
# Before sign in
visit user_path(not_found_id)
expect(page).to have_text not_found_message
# After sign in
visit sign_in_path
fill_in :user_email, with: @user1.email
fill_in :user_password, with: @user1.password
click_on :sign_in_button
visit user_path(not_found_id)
expect(page).to have_text not_found_message
end
end
基本的には今までと変わりありませんね。
一つだけexceptionの検証の仕方だけ新出があるのでそれの説明を。
expect{}.to raise_exception()
今までと違うのはexpect
の検証ターゲットを()
ではなく{}
でかこっていることですね。
そしてraise_exception
の後に期待する例外を記述します。
今回は@user2
のid
に+1
したid
のユーザーを検索しています。id
はシーケンシャルに払い出されるので@user2
よりも大きいid
を持っているユーザーはいないはず。
なのでUser.find(not_found_id)
はActiveRecord::RecordNotFound
の例外が発生するはずです。
はい。ここまでで今のところ考えられる全てのテストケースをコーディングしてみました。
テストを実行してみましょう!
# rspec
Capybara starting Puma...
* Version 4.3.1 , codename: Mysterious Traveller
* Min threads: 0, max threads: 4
* Listening on tcp://127.0.0.1:46237
......................................................................
Finished in 1 minute 21.27 seconds (files took 5.82 seconds to load)
70 examples, 0 failures
全てパスしてますね!
もしパスしないテストケースがある場合は、もう一度アプリのコードかテストコードを見返してみてくださいね。
また、あえてエラーになるようにテストコードを書き直してみてエラーになることを確認してみるのも面白いと思います。
さて、では本日はここまでにしましょう!
まとめ
今日は今まで作ってきたアプリに対してテストコードをコーディングしてみました。
これによって今後リファクタリングの都度自動テストを回すだけで全ての動作を確認することができるようになりましたね。
テスト自動化、楽しいですよね??
テスト自動化は新しい機能を作ったり、アプリの仕様自体を変更する時にテストコードも記述する必要があるのでその分稼働が必要になることもあります。
しかし、リファクタリングや新機能開発時のデグレテストを簡略化でき、自動テストをパスしていればデプロイを自信をもって行える安心感を得ることができます。特にアプリが大きくなっていくと、これは稼働以上に嬉しい恩恵です。
今後はこのハンズオンでもTDDで開発を進めていきます!
では、次回も乞うご期待!ここまでお読みいただきありがとうございました!
Next: Coming Soon
後片付け
では後片付けしていきますー。
前回もお話した通り、RSpecのシステムテストはテストが終わるとDBを勝手にリセットしてくれます。
ので、コンテナを落として終了ですね。
# exit
$ docker-compose down
本日のソースコード
Reference
- 【Rails】こわくない!TDD/BDD・テスト自動化はじめの一歩ハンズオン! - Qiita
- 使えるRSpec入門・その4「どんなブラウザ操作も自由自在!逆引きCapybara大辞典」 - Qiita
- Class: Capybara::Node::Element — Documentation for jnicklas/capybara (master)
- 【FeatureSpec】404 / 500ページを表示させる - Qiita