14
13

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 5 years have passed since last update.

【Rails】こわくない!TDD/BDD・テスト自動化はじめの一歩ハンズオン!

Last updated at Posted at 2019-11-12

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
app/models/item.rb
class Item < ApplicationRecord
  validates :name, presence: true
  validates :name, length: { maximum: 20 }
end

少しテストを面白くするために、nameにnot nullと20文字以内のバリデーションをかけました。

Structure

テストコードはspec/system/*_spec.rbファイルを作成して記述していきます。

テストコードの基本的な構文は以下の通りです。

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)に直接アクセスできること

では、これらのテストコードを書いてみましょう!

spec/system/us1_access_page_spec.rb
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.rbroot to:に指定したルートパスにアクセスする操作ということになります。

今回はscaffoldを使っているので、config/routes.rbresources :itemsが定義されています。
このおかげでテストコードに書いたような割り振りのprefixがつけられているというわけです。prefixは

$ docker-compose run web rails routes

で確認できます。

prefixはルーティング毎にconfig/routes.rbで定義できます。

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で操作するようにしておけばデザインや文字をいじったとしてもテストコードを変更することなく開発を進められるのでそうしています。

app/views/items/index.html.erb
<%# "New Item"リンクに"new_item_link"のidをつける %>
<%= link_to 'New Item', new_item_path, id: :new_item_link %>
app/views/items/show.html.erb
<%# "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 %>
app/views/items/_form.html.erb
<%# "Create Item"ボタンに"submit_item_button"のidをつける %>
<%= form.submit nil, id: :submit_item_button %>
app/views/items/new.html.erb
<%# "Back"リンクに"back_link"のidをつける %>
<%= link_to 'Back', items_path, id: :back_link %>
app/views/items/edit.html.erb
<%# "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指定で要素を操作することができます。

ではテストコードを書いていきましょう!

spec/system/us2_create_todo_items_spec.rb
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_ininput要素に文字入力するメソッドです。targetinputidまたはnameなどを指定します。with以降の文字列が入力されます。

expect(input_target.value).to eq text

input_targetinput要素です。.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を描かれているのでShowEditDestroyリンクにclassを付与して操作しやすいようにしておきます。複数存在するのでidは付与できないです。
item.nameを検証するためにtdにもclassを付与します。

app/views/items/index.html.erb
  <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で指定できるようになりました。
テストコードは以下のようになるかと思います。

spec/system/us3_show_todo_items_spec.rb
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アプリ一覧ページへ遷移すること

もちろんテストコードも似たような形でかけるかなと思います。

spec/system/us4_update_todo_items_spec.rb
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のつけ忘れには気をつけてください。

ではテストコードをみていきましょう!

spec/system/us5_destroy_todo_items_spec.rb
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のレコードが存在しないことを検証できます。reloadfind(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アプリでテスト自動化をやってみました。
最初は時間もかかるなーという印象があると思いますが、アプリを改善し続けていく上で自動化は必須の技術になっていると僕は思います。仕様も明確になりますしデグレの心配もなし。
テスト自動化はやってみたいけど、何かしらの理由で見送っている人やプロダクトに少しでも寄与できる記事になっていれば幸いです。

Reference

14
13
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
14
13

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?