経緯
先日RailsTutorialを修了し、自分でコードを書き始めた途端にテストを書かなくなってしまいました。
これは非常に良くないと思ったので、勉強した内容について簡単にまとめておきます。
この記事で(多分)わかること
- テストを書くことのメリット
- RailsTutorialに登場する単体テスト・機能テスト・統合テストの違い
- それぞれのテストが何を対象としているか、どんなテストを書けばいいのか
テストを書くことのメリット
まずは勉強のモチベーションを上げるために、テスト自動化ができると何が嬉しいのかを調べました。
1. テスト作業の効率化と工数の削減
テストを手動で行っていた工数を大幅に削減し、開発に時間を割けるようになる。
リアルタイムでテストを実行できるため、誤ってコードを改変したときにどこが悪かったのか即座に検知できる。2. テスト作業におけるヒューマンエラーの排除
機械が何度でも正確にテストを繰り返してくれるため、人的ミスを排除することができる。
3. 手動では実現できなかったテスト作業を実現
「数万件の同時アクセス」などの負荷テストや、イレギュラーなテストパターンを容易に実行できる。
ポートフォリオ作成に取り掛かる初学者にとっては、1.が感じやすいメリットだと思います。
「上手くいっていたものを動作しないコードに書き換えてしまう」ことが頻発してしまいそうなので、それをリアルタイムで検知してくれるのは非常に頼もしく、安心してリファクタリングできそうです。
先にまとめ
テストの種類 | テスト対象 | テスト方法 | テストを記載するファイル |
---|---|---|---|
単体テスト | モデル | モデルに記載された機能に対して、テストデータを投入して結果を確認する | test/models |
機能テスト | コントローラ | コントローラに対してHTTPリクエストを発行し、コントローラの振る舞いを確認する | test/controllers |
統合テスト | モデル・コントローラ・ビュー | 想定されるユーザの操作フローを模倣して、モデル・コントローラ・ビューの一連の振る舞いを確認する | test/integration |
以下でそれぞれのテストについて記載していきます。
単体テスト
単体テストはモデル(/app/models/xxx.rb)単位に記述します。
モデルに記載したバリデーションチェック等の機能一つに対して、対応するテストを一つ記述するイメージです。
例)
class User < ApplicationRecord
validates :name, presence: true, length: { maximum: 50 }
end
def setup
@user = User.new(name: "Example User", email: "user@example.com",
password: "foobar", password_confirmation: "foobar")
end
#user.nameのバリデーションに関するテスト
test "name should be present" do
@user.name = " "
assert_not @user.valid?
end
test "name should not be too long" do
@user.name = "a" * 51
assert_not @user.valid?
end
上記の例ではuserモデルのnameというカラムに以下のバリデーションチェックを設けています。
1. 名前が空白でないこと
2. 50字以内であること
それに対して以下のテスト実行しています。
1. 名前が空白の@userを作成し@userが無効になること
2. 名前が51文字の@userを作成し@userが無効になること
上記は非常に単純な例ですが、複雑な機能でもやっていることは一緒のような気がします。
モデルに一つ機能を追加するたびに一つテストも追加するようなイメージですかね。
機能テスト
機能テストはコントローラ(/app/controllers/xxx_controller.rb)単位に記述します。
「def new」「def create」などのアクション一つ一つに対して記述するイメージです。
コントローラにHTTPリクエスト(GET,POST,PATCH,DELETE)を送り、
それに対するコントローラの振る舞いが想定通りであるかどうかを確認します。
例)
def create
@user = User.new(user_params)
#ユーザの保存に成功した場合は、①flashにメッセージを格納し②ログインした後③ユーザ詳細画面にリダイレクトする
if @user.save
flash[:success] = "Welcome to App!"
login @user
redirect_to @user
#ユーザの保存に失敗した場合は、再びユーザ登録画面にリダイレクトする
else
render 'new'
end
end
#有効なユーザ情報を登録した場合のcreateアクションの振る舞いをテストする
test "should get create with valid user" do
#ユーザ登録画面にアクセスし、正常応答を得る
get new_user_path
assert_response :success
#有効なユーザ登録のPOSTリクエストを送り、ユーザの数が1増えることを確認する
assert_difference 'User.count', +1 do
post users_path, params: { user: { name: "Example User",
email: "user@example.com",
password: "foobar",
password_comfirmation: "foobar"} }
end
#flashにメッセージが格納されている(ユーザ登録成功のメッセージ)ことを確認する
assert_not flash.empty?
#ユーザ登録後ログインできていることを確認する
assert is_logged_in?
end
#無効なユーザ情報を登録した場合のcreateアクションの振る舞いをテストする
test "should get create with invalid user" do
#ユーザ登録画面にアクセスし、正常応答を得る
get new_user_path
assert_response :success
#無効なユーザ登録のPOSTリクエストを送り、ユーザの数が増減しないことを確認する
assert_no_difference 'User.count' do
post users_path, params: { user: { name: " ",
email: " ",
password: " ",
password_comfirmation: " "} }
end
#flashにメッセージが格納されている(ユーザ登録失敗のメッセージ)ことを確認する
assert flash.empty?
#ユーザ登録後ログインできていないことを確認する
assert_not is_logged_in?
#ユーザ登録画面にリダイレクトされていることを確認する
assert_template 'users/new'
end
上記の例では、userコントローラのcreateアクションの想定される振る舞いは以下の2通りです。
1. 登録時のユーザ情報が有効である場合、ユーザを登録しユーザ詳細画面に遷移する
2. 登録時のユーザ情報が無効である場合、再度ユーザ情報入力画面に遷移する
それに対して以下のテストを実行しています。
1. 有効なユーザ情報のPOSTリクエストを送信し、ユーザ詳細画面に遷移すること
2. 無効なユーザ情報のPOSTリクエストを送信し、ユーザ登録画面に戻されること
「アクション一つにつきテスト一つ」というわけではなく、
「リクエストに対して想定される振る舞いの数のテストが必要」という点がポイントかなと思いました。
統合テスト(UIテスト)
統合テストはユーザーの実際の操作を想定し、操作のフローに伴うアプリの挙動を確認します。
テストを書くにあたってアプリケーションでユーザがどのような操作をできるのかを洗い出す必要がありそうです。
例)
require 'test_helper'
class SiteLayoutTest < ActionDispatch::IntegrationTest
test "layout links" do
#ホーム画面に遷移する
get root_path
#遷移先のURLが想定されたものであることを確認する
assert_template 'static_pages/home'
#遷移先画面で表示されているリンクの数が想定された通りであることを確認する
assert_select "a[href=?]", root_path, count: 2
assert_select "a[href=?]", help_path
assert_select "a[href=?]", about_path
assert_select "a[href=?]", contact_path
end
end
上記の例では、「ユーザがホーム画面に遷移する」という操作を想定ししています。
- ルートパスにgetリクエストを送り、ホーム画面に遷移することを確認する
- 遷移したホーム画面にて、画面上に表示しているリンクが想定通りであることを確認する
前述までの単体テスト・機能テストでは死んでいるリンクなどを検知できません。
統合テストを書くことでビューのバグも検知できるため、自分でブラウザを開いて一個一個クリックする必要がなくなりそうです。
本当にあっている?
ここまで記載しましたが、自分で怪しいと思っている点が2点あります。
機能テストの例で出したものは本当に機能テストになっているか?
機能テストの例としたcreateコントローラのテストは「ユーザのログイン」操作を模倣した統合テストとも捉えられます。
統合テストを書こうとしても上述した機能テストの繰り返しのようになってしまう気がしました。
①userコントローラにgetリクエストを送り、アクセスできることのテスト
②userコントローラに有効なpostリクエストを送り、ユーザ登録できることのテスト
③userコントローラに無効なpostリクエストを送り、ユーザ登録できないことのテスト
とリクエスト一発に対して何が帰ってくるかを細かく区切った一つ一つが機能テストになるでしょうか?「テストを記載するファイル」について別にルールはないのでは?
上記の「test/controllers」に記載しているテストは「test/integration」の中に記載しても動きます。
極端に言えば全てのテストを同じファイルに記載しても、テストは実行可能なのではないでしょうか。
Tutorialで分かりやすいフォルダ構成を決めているだけで、テストを書くファイルは実際には自由?
・・・有識者の方、ご指摘いただけると助かります。
最後に
RailsTutorialは出てきたテストをそのままコピーしていたので、
それぞれのテストの違いを理解しないまま進めてしまっていたので、今回整理できてよかったです。
誤っている記述などあれば、コメントにてご教示いただけると助かります。