Abstract
TDD/BDDは素晴らしいです!テスト自動化は感動的です!この感動を多くの人に感じてもらいたい!
でも実際、テスト自動化と聞くとハードルありますよね。テストコードって難しいんじゃないかとか、時間かかるし手動でテストすればいいんじゃないかとか。でもそうじゃない!
ということで、この記事ではハードルをものすごく下げて
- TDD/BDDのことを語る
- RailsアプリのE2Eテスト自動化をハンズオンする
をしてみたいと思います。
最初にお断りですが、E2EテストをするためのRSpecやCapybaraのいろいろな使い方については語らないです。そのあたりは
などがとても参考になるので...
同じ操作・検証をするにも色々な書き方があるので、今回の記事の書き方はあくまで一例ということでお願いします!
なぜTDD/BDDなのか
みなさんは
- あいつらのシステム要件が何言ってるかわからん
- どこまで作ったら開発終わるのかわからん
- テスト工程で初めて理解した。齟齬ってたことを。
みたいな経験ないですか?もしくは
- システム要件って何提示すればいいかわからん
- 開発者たち、念入りにテストしすぎじゃね?
- 齟齬が発覚したときにはもう直せないと言われた、最初からそのつもりだったのに
みたいな経験(プロダクトオーナー目線)。
TDD/BDDはこいつらを解決できます!
TDD/BDDはTest Driven Development(テスト駆動開発)とBehavior Driven Development(振る舞い駆動開発)のことです。"駆動開発"は開発において最も大切にしていること、を表していると思うのですが、そうなると"テスト"と"振る舞い"を大切にしている開発のことです。
TDDは最初にテスト定義しましょう、という開発です。最初っていつ?というと最初、つまりシステム要件定義の段階だと私は思っています。
たとえばウォーターフォール開発では、要件定義・基本設計・詳細設計などのフェーズを踏みコーディングをした後、各フェーズのoutputをinputとしてテストを設計していくかと思いますが、じゃあ最初から各フェーズのoutputがテストならいんじゃない?という考え方ですかね。
TDDのいいところは、"明確"になることです。テストはOKかNGしかありません(OK/NGしかないように定義しなければいけません)。
どんなに読みやすい設計書があろうと、軽量なコードがあろうと、テストがNGならダメです。TDDは最初にテストを定義することで、頑張ってコーディングしたけどテスト設計したら要件と違った、ということを防いでくれます。
また、最初にテストを定義することで、テストがAll OKになることが完了であることが"明確"になります。
BDDはTDDの派生として、プロダクトの振る舞いを定義します。BDDの方がよりプロダクトのユーザーの行動に近いテストを定義するイメージになります。
わたしのTDD/BDD
わたしの場合は、システム要件としてアジャイル開発で用いられるユーザーストーリーを定義することから始めます。
Whoとして、Whatしたい、なぜならWhyからだ
という型に当てはめて、ストーリーを作っていきます。機能一覧に近いですが、ユーザーの行動が基準に置かれているのでいくつかの機能を束ねていたり、機能の一部のみが必要だったりもします。
例えば、弊サービス4Q4T(チームビルディングサポートツール)のケースだと、
- チームリーダーとして、チームを作成したい、なぜなら自分のチームで4Q4Tを使いよりよいチームを構築したいからだ
- チームリーダーとして、簡単な方法でメンバーを招待したい、なぜなら登録ハードルが低い方がメンバーが登録してくれやすいからだ
- チームメンバーとして、ドラッカー風エクササイズの質問に答えてほかのメンバーに見せたい、なぜならチームメンバーと期待を確認し合いたいからだ
みたいなものがストーリーになります。
さらにこのユーザーストーリーに対して、受入条件を定義しています。
受入条件の型は
Who が Where で How の状態で What した場合、 Who は Where で What なること。
を基本としています。BDDのフレームワークツールなどが色々あるのですが、それらを参考に自分が使いやすい形をして求めて今はこの型にしてます。
例えば、上の「チームリーダーとして、チームを作成したい」というユーザーストーリーに対しては
- ユーザーはトップページにアクセスできること
- ユーザーがトップページで"Create a team"ボタンを選択した場合、ユーザーはチーム作成ページへ遷移すること
- ユーザーはチーム作成ページでチーム名を入力できること
- ユーザーがチーム作成ページでチーム名が未入力の状態で"Create"ボタンを選択した場合、ユーザーはチーム作成ページでチーム名未入力のエラーメッセージを受け取れること
- ユーザーがチーム作成ページでチーム名を入力した状態で"Create"ボタンを選択した場合、チームが作成され、ユーザーはそのチームのチームページへ遷移すること
といった具合です。こうやって型を作っておくことで、テストを素早く漏れなく定義できます。
テストは定義したけど…
ここまで定義できてしまえば、このテストが通るコードを書けばいいだけです。
ただ、少しコードをいじったら毎回テストって無理ですよね。リファクタ前と全く同じテストになってますか?デグレなんてありえないけどそのコードが他の機能に影響を与えないとも言い切れない。なので影響調査に時間をかけて慎重に、ってそれじゃ時代の変化にプロダクトが追いつかない。
そこでテスト自動化(Test automation)です!
テスト自動化は、テストをコーディングしてプログラムとして実行することで、プロダクトが定義したテスト通りの振る舞いをしているかをテストすることです。テストコードを書くのにはそれなりの時間が必要ですが、テスト自体は人手でやるよりも圧倒的に速く、再現性もあり、一度作成すれば何回も使いまわせます。
いいことだらけのテスト自動化、やるしかないですよね。ということで、今回はRailsアプリでテスト自動化ハンズオンを開催します!!
テスト自動化ハンズオン
準備
まずは、こちらからベースのRailsアプリケーションをcloneしてください。
$ git clone git@github.com:at946/rails-test-automation-hands-on.git
$ cd rails-test-automation-hands-on
$ docker-compose build
$ docker-compose run web yarn install --check-files
$ docker-compose run web rails db:create
$ mkdir spec/system
Rails on Docker(alpine)でdocker-seleniumを使わないでSelenium+RSpec+Capybaraでテスト自動化してみる - Qiita に記載している手順ですでにSelenium, RSpec, CapybaraがインストールされたRails on Dockerアプリです。
- rspec-rails : Railsのテストフレームワーク
- selenium-webdriver : ブラウザ操作をプログラムで実行できるテストツール
- capybara : RSpecやSeleniumのコードを書きやすくしてくれたりするテストフレームワーク
今回のハンズオンでは、めちゃくちゃシンプルなTodolistアプリをテストしていきましょう。
まず、Todolistアプリをscaffoldで作成していきます。
$ docker-compose run web rails g scaffold item name:string
$ docker-compose run web rails db:migrate
class Item < ApplicationRecord
validates :name, presence: true
validates :name, length: { maximum: 20 }
end
少しテストを面白くするために、name
にnot nullと20文字以内のバリデーションをかけました。
Structure
テストコードはspec/system/
に*_spec.rb
ファイルを作成して記述していきます。
テストコードの基本的な構文は以下の通りです。
feature "ユーザーストーリー", type: :system, js: true do
before :each do
# 各シナリオの前に共通して実行される処理
# モデルの作成などで利用される
# 作成されたモデルは各シナリオ終了時にクリアされる
end
scenario "受入条件1" do
# operation... (操作)
# expect().to... (検証)
end
scenario "受入条件2" do
# operation... (操作)
# expect().to... (検証)
end
end
ファイルはユーザーストーリー単位に作成しておくと管理がしやすくていいと思います。共通的な基本処理、例えばサイトにURL直打ちでアクセスできる、だとか、ヘッダーのロゴをクリックしたらトップページに遷移する、みたいなものも1ファイルとして管理するのもオススメです。
また、テスト実行は以下のコマンドで実行できます。
$ docker-compose run web rspec
特定のファイルのみを指定してテストすることもできます。
$ docker-compose run web rspec spec/system/*_spec.rb
サンプルアプリのユーザーストーリー
まずは今回のサンプルアプリのユーザーストーリー(US)を定義します。scaffoldに合わせてになりますが、RailsアプリだとRESTfulを大切にすることもあり大体以下のような感じになると思います。(Whyは省略)
- US1: ユーザーとして、ページにアクセスしたい
- US2: ユーザーとして、Todoアイテムを作成したい
- US3: ユーザーとして、Todoアイテムを確認したい
- US4: ユーザーとして、Todoアイテムを更新したい
- US5: ユーザーとして、Todoアイテムを削除したい
おまちかね、ここから先は実際にテストコードを書いていきます。
まず、ユーザーストーリーに対して受入条件を定義して、その後にテストコードを書いていきます。そして、テストコード内の書き方についてコメントを添えます。
【US1】 ユーザーとして、ページにアクセスしたい
受入条件は以下の通り。scaffoldで用意されたViewに正しくアクセスできることです。
- ユーザーはTodoアイテム一覧ページ(
/items
)に直接アクセスできること - ユーザーはTodoアイテム作成ページ(
/items/new
)に直接アクセスできること - ユーザーはTodoアイテム詳細ページ(
/items/{:id}
)に直接アクセスできること - ユーザーはTodoアイテム編集ページ(
/items/{:id}/edit
)に直接アクセスできること
では、これらのテストコードを書いてみましょう!
feature "ユーザーとして、ページにアクセスしたい", type: :system, js: true do
before :each do
@item = Item.create(name: "ほげほげ申請する")
end
scenario "ユーザーはTodoアイテム一覧ページ(/items)に直接アクセスできること" do
visit items_path
expect(page).to have_current_path items_path
expect(page).to have_text "Items"
end
scenario "ユーザーはTodoアイテム作成ページ(/items/new)に直接アクセスできること" do
visit new_item_path
expect(page).to have_current_path new_item_path
expect(page).to have_text "New Item"
end
scenario "ユーザーはTodoアイテム詳細ページ(/items/{:id})に直接アクセスできること" do
visit item_path @item
expect(page).to have_current_path item_path @item
expect(page).to have_text "Name: "
end
scenario "ユーザーはTodoアイテム編集ページ(/items/{:id}/edit)に直接アクセスできること" do
visit edit_item_path @item
expect(page).to have_current_path edit_item_path @item
expect(page).to have_text "Editing Item"
end
end
visit *_path
visit
はパスにGETアクセスする操作です。後ろにURLや名前付きルーティングヘルパー(prefix)を指定して使います。
例えば、visit root_path
と書けば、config/routes.rb
でroot to:
に指定したルートパスにアクセスする操作ということになります。
今回はscaffoldを使っているので、config/routes.rb
にresources :items
が定義されています。
このおかげでテストコードに書いたような割り振りのprefixがつけられているというわけです。prefixは
$ docker-compose run web rails routes
で確認できます。
prefixはルーティング毎にconfig/routes.rb
で定義できます。
Rails.application.routes.draw do
# method path, to: 'controller#action', as: 'prefix'
get 'hoge', to: 'hoges#index', as: 'fuga'
end
この例では、fuga_path
が/hoge
のprefixになっています。
expect(target).to *
expect
は検証をするためのメソッドです。target
に指定したものを検証対象として、to
以降に記載したものと一致するかどうかを検証します。to
の代わりにnot_to
と記載した場合は一致しないことの検証です。
今回のテストコードではexpect(page).to
という使い方をしているのでページ全体に対して検証を行っていると思えばいいと思います。
また、expect(find("#id")).to
のようにid
がid属性として定義されている要素に対して検証したりなどもできます。
have_current_path *_path
have_current_path
で現在表示中のページのパスが*_path
と一致するかを検証しています。
ほかにも、expect(current_path).to eq *_path
という書き方でも同じ検証ができます。
have_text text
have_text
で表示中のページにtext
の文字列が含まれるかを検証しています。
例えば、pathにはアクセスできたけど表示されているページは期待通りでない!となっていないかという観点で、それぞれのページ固有のワードを検証させてみました。
【US2】 ユーザーとして、Todoアイテムを作成したい
受入条件は以下のような感じで。name
に少し制約を入れているので少し条件が多いです。
- ユーザーがTodoアイテム一覧ページで"New Item"リンクを選択した場合、ユーザーはTodoアイテム作成ページへ遷移すること
- ユーザーはTodoアイテム作成ページでアイテム名を入力できること
- ユーザーがTodoアイテム作成ページでアイテム名が未入力の状態で"Create Item"ボタンを選択した場合、Todoアイテムは作成されず、Todoアイテム作成ページでTodoアイテム名未入力のエラーメッセージが表示されること
- ユーザーがTodoアイテム作成ページでアイテム名が21文字以上の状態で"Create Item"ボタンを選択した場合、Todoアイテムは作成されず、Todoアイテム作成ページでTodoアイテム名21文字以上のエラーメッセージが表示されること
- ユーザーがTodoアイテム作成ページでアイテム名が20文字以内の状態で"Create Item"ボタンを選択した場合、Todoアイテムは作成され、ユーザーはTodoアイテム作成成功メッセージが表示されたTodoアイテム詳細ページへ遷移すること
- ユーザーがTodoアイテム作成ページで"Back"リンクを選択した場合、ユーザーはTodoアプリ一覧ページへ遷移すること
テストコードを書く前に、buttonやlinkなど操作する要素にidを定義していきます。xpathやテキスト、cssなどでも操作をすることはできるのですが、idで操作するようにしておけばデザインや文字をいじったとしてもテストコードを変更することなく開発を進められるのでそうしています。
<%# "New Item"リンクに"new_item_link"のidをつける %>
<%= link_to 'New Item', new_item_path, id: :new_item_link %>
<%# "Edit"リンクに"edit_item_link"、"Back"リンクに"back_link"のidをつける %>
<%= link_to 'Edit', edit_item_path(@item), id: :edit_item_link %> |
<%= link_to 'Back', items_path, id: :back_link %>
<%# "Create Item"ボタンに"submit_item_button"のidをつける %>
<%= form.submit nil, id: :submit_item_button %>
<%# "Back"リンクに"back_link"のidをつける %>
<%= link_to 'Back', items_path, id: :back_link %>
<%# "Show"リンクに"show_item_link"、"Back"リンクに"back_link"のidをつける %>
<%= link_to 'Show', @item, id: :show_item_link %> |
<%= link_to 'Back', items_path, id: :back_link %>
これでid指定で要素を操作することができます。
ではテストコードを書いていきましょう!
feature "ユーザーとして、Todoアイテムを作成したい", type: :system, js: true do
scenario "ユーザーがTodoアイテム一覧ページで'New Item'ボタンを選択した場合、ユーザーはTodoアイテム作成ページへ遷移すること" do
visit items_path
click_on :new_item_link
expect(page).to have_current_path new_item_path
end
scenario "ユーザーはTodoアイテム作成ページでアイテム名を入力できること" do
name = "ふがふが申請する"
visit new_item_path
fill_in :item_name, with: name
expect(find("#item_name").value).to eq name
end
scenario "ユーザーがTodoアイテム作成ページでアイテム名が未入力の状態で'Create Item'ボタンを選択した場合、Todoアイテムは作成されず、Todoアイテム作成ページでTodoアイテム名未入力のエラーメッセージが表示されること" do
name = ""
msg_error = "Name can't be blank"
item_count = Item.count
visit new_item_path
fill_in :item_name, with: name
click_on :submit_item_button
expect(Item.count).to eq item_count
expect(page).to have_text msg_error
end
scenario "ユーザーがTodoアイテム作成ページでアイテム名が21文字以上の状態で'Create Item'ボタンを選択した場合、Todoアイテムは作成されず、Todoアイテム作成ページでTodoアイテム名21文字以上のエラーメッセージが表示されること" do
name = "21文字以上のTodoアイテムは登録できぬ"
msg_error = "Name is too long (maximum is 20 characters)"
item_count = Item.count
visit new_item_path
fill_in :item_name, with: name
click_on :submit_item_button
expect(Item.count).to eq item_count
expect(page).to have_text msg_error
end
scenario "ユーザーがTodoアイテム作成ページでアイテム名が20文字以内の状態で'Create Item'ボタンを選択した場合、Todoアイテムは作成され、ユーザーはTodoアイテム作成成功メッセージが表示されたTodoアイテム詳細ページへ遷移すること" do
name = "ふがふが申請する"
msg_success = "Item was successfully created."
item_count = Item.count
visit new_item_path
fill_in :item_name, with: name
click_on :submit_item_button
expect(Item.count).to eq item_count + 1
expect(page).to have_current_path item_path Item.last
expect(page).to have_text msg_success
expect(page).to have_text name
end
scenario "ユーザーがTodoアイテム作成ページで'Back'リンクを選択した場合、ユーザーはTodoアプリ一覧ページへ遷移すること" do
visit new_item_path
click_on :back_link
expect(page).to have_current_path items_path
end
end
click_on target
click_on
はリンクまたはボタンをクリックするメソッドです。target
として、:new_item_link
や、:back_link
など、idを指定することでそのidをもつ要素をクリックできます。
ちなみに、リンク・ボタン以外はこれではクリックできないです。例えば
find("#id").click
fill_in target, with: text #input要素に入力する
fill_in
はinput
要素に文字入力するメソッドです。target
にinput
のid
またはname
などを指定します。with
以降の文字列が入力されます。
expect(input_target.value).to eq text
input_target
はinput
要素です。.value
とすることでinput
要素に入力されている値を検証できます。
eq
を使ってtext
と一致しているかを検証しています。
expect(Item.count).to eq item_count
item_count = Item.count
# なんか色々操作
expect(Item.count).to eq item_count
という検証の仕方をしてます。操作前にモデルの数をitem_count
の変数に代入しておいて、操作後のモデルの数と比較するというやり方です。モデルが操作によって作られるならitem_count + 1
、エラーで変わらない、またはアップデートだから変わらないならitem_count
、削除されるならitem_count - 1
といった具合です。
【US3】 ユーザーとして、Todoアイテムを確認したい
こちらの受入条件はこんな感じかな。
- ユーザーはTodoアイテム一覧ページで全てのTodoアイテムを更新日昇順で閲覧できること
- ユーザーがTodoアイテム一覧ページであるTodoアイテムの"Show"リンクを選択した場合、ユーザーはそのTodoアイテムのTodoアイテム詳細ページへ遷移すること
- ユーザーがTodoアイテム編集ページで"Show"リンクを選択した場合、ユーザーはTodoアイテム詳細ページへ遷移すること
- ユーザーはTodoアイテム詳細ページでそのTodoアイテムのアイテム名を確認できること
- ユーザーがTodoアイテム詳細ページで"Back"リンクを選択した場合、ユーザーはTodoアイテム一覧ページへ遷移すること
こちらもはじめに、classを指定しておきます。Todoアイテム一覧ページではTodoアイテムがいっぱい並ぶのですが、同じようにリンクも並びます。each
でviewを描かれているのでShow
、Edit
、Destroy
リンクにclass
を付与して操作しやすいようにしておきます。複数存在するのでid
は付与できないです。
item.name
を検証するためにtd
にもclass
を付与します。
<tbody>
<% @items.each do |item| %>
<tr>
<td class="item-name"><%= item.name %></td>
<td><%= link_to 'Show', item, class: 'show-link' %></td>
<td><%= link_to 'Edit', edit_item_path(item), class: 'edit-link' %></td>
<td><%= link_to 'Destroy', item, method: :delete, class: 'destroy-link', data: { confirm: 'Are you sure?' } %></td>
</tr>
<% end %>
</tbody>
これで繰り返しの要素に対してもclass
で指定できるようになりました。
テストコードは以下のようになるかと思います。
feature "ユーザーとして、Todoアイテムを確認したい", type: :system, js: true do
before :each do
@item1 = Item.create(name: "ほげほげ申請する")
@item2 = Item.create(name: "ふがふが申請する")
end
scenario "ユーザーはTodoアイテム一覧ページで全てのTodoアイテムを更新日昇順で閲覧できること" do
visit items_path
expect(all(".item-name")[0]).to have_text @item1.name
expect(all(".item-name")[1]).to have_text @item2.name
@item1.update(name: "ほげほげほげほげ申請する")
visit items_path
expect(all(".item-name")[0]).to have_text @item2.name
expect(all(".item-name")[1]).to have_text @item1.name
end
scenario "ユーザーがTodoアイテム一覧ページであるTodoアイテムの'Show'リンクを選択した場合、ユーザーはそのTodoアイテムのTodoアイテム詳細ページへ遷移すること" do
visit items_path
all(".show-link")[0].click
expect(page).to have_current_path item_path @item1
end
scenario "ユーザーがTodoアイテム編集ページで'Show'リンクを選択した場合、ユーザーはTodoアイテム詳細ページへ遷移すること" do
visit edit_item_path @item1
click_on :show_item_link
expect(page).to have_current_path item_path @item1
end
scenario "ユーザーはTodoアイテム詳細ページでそのTodoアイテムのアイテム名を確認できること" do
visit item_path @item1
expect(page).to have_text @item1.name
end
scenario "ユーザーがTodoアイテム詳細ページで'Back'リンクを選択した場合、ユーザーはTodoアイテム一覧ページへ遷移すること" do
visit item_path @item1
click_on :back_link
expect(page).to have_current_path items_path
end
end
all(target)[index]
all
でページ内の全ての対象のclassを取得してます。[index]
でその中の何個目の要素をターゲットにするかを指定できます。index
は0から始まるのに注意です。
【US4】 ユーザーとして、Todoアイテムを更新したい
お次はこんなかんじの受入条件。更新は作成とviewが共通化されていることもあり、似た観点になります。
- ユーザーはTodoアイテム一覧ページであるTodoアイテムの"Edit"リンクを選択した場合、ユーザーはそのTodoアイテムのTodoアイテム編集ページへ遷移すること
- ユーザーがTodoアイテム詳細ページで"Edit"リンクを選択した場合、ユーザーはTodoアイテム編集ページへ遷移すること
- Todoアイテム編集ページでアイテム名はデフォルトで現在のアイテム名が入力されていること
- ユーザーはTodoアイテム編集ページでアイテム名を入力できること
- ユーザーがTodoアイテム編集ページでアイテム名が未入力の状態で"Update Item"ボタンを選択した場合、Todoアイテムは更新されず、Todoアイテム編集ページでTodoアイテム名未入力のエラーメッセージが表示されること
- ユーザーがTodoアイテム編集ページでアイテム名が21文字以上の状態で"Update Item"ボタンを選択した場合、Todoアイテムは更新されず、Todoアイテム編集ページでTodoアイテム名21文字以上のエラーメッセージが表示されること
- ユーザーがTodoアイテム編集ページでアイテム名が20文字以内の状態で"Update Item"ボタンを選択した場合、Todoアイテムは更新され、ユーザーはTodoアイテム更新成功メッセージが表示されたTodoアイテム詳細ページへ遷移すること
- ユーザーがTodoアイテム編集ページで"Back"リンクを選択した場合、ユーザーはTodoアプリ一覧ページへ遷移すること
もちろんテストコードも似たような形でかけるかなと思います。
feature "ユーザーとして、Todoアイテムを更新したい", type: :system, js: true do
before :each do
@item1 = Item.create(name: "ほげほげ申請する")
end
scenario "ユーザーはTodoアイテム一覧ページであるTodoアイテムの'Edit'リンクを選択した場合、ユーザーはそのTodoアイテムのTodoアイテム編集ページへ遷移すること" do
visit items_path
all(".edit-link")[0].click
expect(page).to have_current_path edit_item_path @item1
end
scenario "ユーザーがTodoアイテム詳細ページで'Edit'リンクを選択した場合、ユーザーはTodoアイテム編集ページへ遷移すること" do
visit item_path @item1
click_on :edit_item_link
expect(page).to have_current_path edit_item_path @item1
end
scenario "Todoアイテム編集ページでアイテム名はデフォルトで現在のアイテム名が入力されていること" do
visit edit_item_path @item1
expect(find("#item_name").value).to eq @item1.name
end
scenario "ユーザーはTodoアイテム編集ページでアイテム名を入力できること" do
name = "ほげほげふがふが申請する"
visit edit_item_path @item1
fill_in :item_name, with: name
expect(find("#item_name").value).to eq name
end
scenario "ユーザーがTodoアイテム編集ページでアイテム名が未入力の状態で'Update Item'ボタンを選択した場合、Todoアイテムは更新されず、Todoアイテム編集ページでTodoアイテム名未入力のエラーメッセージが表示されること" do
name = ""
msg_error = "Name can't be blank"
visit edit_item_path @item1
fill_in :item_name, with: name
click_on :submit_item_button
expect(@item1).to eq Item.find(@item1.id)
expect(page).to have_text msg_error
end
scenario "ユーザーがTodoアイテム編集ページでアイテム名が21文字以上の状態で'Update Item'ボタンを選択した場合、Todoアイテムは更新されず、Todoアイテム編集ページでTodoアイテム名21文字以上のエラーメッセージが表示されること" do
name = "21文字以上のTodoアイテムは登録できぬ"
msg_error = "Name is too long (maximum is 20 characters)"
visit edit_item_path @item1
fill_in :item_name, with: name
click_on :submit_item_button
expect(@item1).to eq Item.find(@item1.id)
expect(page).to have_text msg_error
end
scenario "ユーザーがTodoアイテム編集ページでアイテム名が20文字以内の状態で'Update Item'ボタンを選択した場合、Todoアイテムは更新され、ユーザーはTodoアイテム更新成功メッセージが表示されたTodoアイテム詳細ページへ遷移すること" do
name = "ほげほげふがふが申請する"
msg_success = "Item was successfully updated."
visit edit_item_path @item1
fill_in :item_name, with: name
click_on :submit_item_button
update_item1 = Item.find(@item1.id)
expect(@item1.name).not_to eq update_item1.name
expect(page).to have_current_path item_path @item1
expect(page).to have_text msg_success
expect(page).to have_text "Name: #{update_item1.name}"
expect(page).not_to have_text "Name: #{@item1.name}"
end
scenario "ユーザーがTodoアイテム編集ページで'Back'リンクを選択した場合、ユーザーはTodoアプリ一覧ページへ遷移すること" do
visit edit_item_path @item1
click_on :back_link
expect(page).to have_current_path items_path
end
end
expect(model).to eq Item.find(model.id)
@item1 = Item.create(name: "hoge")
# 操作
expect(@item1).to eq Item.find(@item1.id)
というような使い方でモデルの更新がないことを検証してみました。
@item1
は操作前に変数化されており、Item.find(@item1.id)
は操作後の@item1
を検索した結果です。両者が一致しているということは操作後もupdateは行われなかったことを表しています。ほかにもそれぞれの更新されるかもしれない属性(今回ならname
)やupdated_at
を比較しても検証ができそうです。
実際、更新されたことの確認にはname
の値が一致しないことを検証しました。
【US5】 ユーザーとして、Todoアイテムを削除したい
最後のユーザーストーリーです。受入条件は以下の通り。
- ユーザーがTodoアイテム一覧ページで"Destroy"リンクを選択した場合、Todoアイテム削除確認ダイアログが表示されること
- ユーザーがTodoアイテム削除確認ダイアログで"キャンセル"を選択した場合、Todoアイテムは削除されず、Todoアイテム削除確認ダイアログが閉じること
- ユーザーがTodoアイテム削除確認ダイアログで"OK"を選択した場合、Todoアイテムは削除され、Todoアイテム削除確認ダイアログが閉じ、Todoアイテム一覧ページでTodoアイテム削除成功メッセージが表示されること
destroy
の際はdata: { confirm: * }
が使われており、jsのダイアログが使われていますのでちょっと今までとはテストコードも違ってきます。
feature
のオプションのjs: true
がない場合、rack_test
が実行される設定になっています(spec/rails_helper.rb
参照)。rack_test
は高速で実行されますが、jsに対応していないのでこの章のテストはかならず失敗します。js: true
のつけ忘れには気をつけてください。
ではテストコードをみていきましょう!
feature "ユーザーとして、Todoアイテムを削除したい", type: :system, js: true do
before :each do
@item = Item.create(name: "ほげほげ申請する")
end
scenario "ユーザーがTodoアイテム一覧ページで'Destroy'リンクを選択した場合、Todoアイテム削除確認ダイアログが表示されること" do
visit items_path
page.dismiss_confirm("Are you sure?") do
all(".destroy-link")[0].click
end
end
scenario "ユーザーがTodoアイテム削除確認ダイアログで'キャンセル'を選択した場合、Todoアイテムは削除されず、Todoアイテム削除確認ダイアログが閉じること" do
visit items_path
page.dismiss_confirm do
all(".destroy-link")[0].click
end
expect(page).to have_current_path items_path
expect{@item.reload}.not_to raise_error
end
scenario "ユーザーがTodoアイテム削除確認ダイアログで'OK'を選択した場合、Todoアイテムは削除され、Todoアイテム削除確認ダイアログが閉じ、Todoアイテム一覧ページでTodoアイテム削除成功メッセージが表示されること" do
visit items_path
expect(page).to have_text @item.name
page.accept_confirm do
all(".destroy-link")[0].click
end
expect(page).to have_current_path items_path
expect{@item.reload}.to raise_error(ActiveRecord::RecordNotFound)
expect(page).not_to have_text @item.name
end
end
page.dismiss_confirm text do ~ end #confirmでキャンセルを選択する
block内の操作が終わったらconfirmで"キャンセル"を選択する操作です。
ついでにtext
がそのconfirmに表示されているかも検証してくれます。
page.accept_confirm text do ~ end #confirmでOKを選択する
confirmで"OK"を選択するバージョンです。
expect{OPE}.to raise_error(ERROR) #例外が発生することを検証する
OPE
した時にエラーが出ないかを検証しています。
OPE
としてModel.find(id)
、ERROR
としてActiveRecord::RecordNotFound
を指定すれば、そのid
のレコードが存在しないことを検証できます。reload
はfind(id)
しているだけなのでこれを利用。
エラーがないことを検証する場合はexpect{OPE}.not_to raise_error
だけでOKです。
confirmの検証はちょっと癖がありますが、これで検証できます。
$ docker-compose run web rspec
Starting rails-test-automation-hands-on_db_1 ... done
Capybara starting Puma...
* Version 4.3.0 , codename: Mysterious Traveller
* Min threads: 0, max threads: 4
* Listening on tcp://127.0.0.1:46549
..........................
Finished in 12.82 seconds (files took 4.34 seconds to load)
26 examples, 0 failures
成功!!
Conclusion
今回は、TDD/BDD、テスト自動化(Selenium+RSpec+Capybara)の初めの一歩としてRailsのscaffoldアプリでテスト自動化をやってみました。
最初は時間もかかるなーという印象があると思いますが、アプリを改善し続けていく上で自動化は必須の技術になっていると僕は思います。仕様も明確になりますしデグレの心配もなし。
テスト自動化はやってみたいけど、何かしらの理由で見送っている人やプロダクトに少しでも寄与できる記事になっていれば幸いです。