LoginSignup
0
0

More than 3 years have passed since last update.

【初学者向け】単体テスト・機能テスト・統合テストを書こう【Rails Tutorial】

Posted at

経緯

先日RailsTutorialを修了し、自分でコードを書き始めた途端にテストを書かなくなってしまいました。
これは非常に良くないと思ったので、勉強した内容について簡単にまとめておきます。

この記事で(多分)わかること

  • テストを書くことのメリット
  • RailsTutorialに登場する単体テスト・機能テスト・統合テストの違い
  • それぞれのテストが何を対象としているか、どんなテストを書けばいいのか

テストを書くことのメリット

まずは勉強のモチベーションを上げるために、テスト自動化ができると何が嬉しいのかを調べました。

1. テスト作業の効率化と工数の削減

テストを手動で行っていた工数を大幅に削減し、開発に時間を割けるようになる。
リアルタイムでテストを実行できるため、誤ってコードを改変したときにどこが悪かったのか即座に検知できる。

2. テスト作業におけるヒューマンエラーの排除

機械が何度でも正確にテストを繰り返してくれるため、人的ミスを排除することができる。

3. 手動では実現できなかったテスト作業を実現

「数万件の同時アクセス」などの負荷テストや、イレギュラーなテストパターンを容易に実行できる。

参考:webrage-テスト自動化とは?テスト自動化のメリットについて

ポートフォリオ作成に取り掛かる初学者にとっては、1.が感じやすいメリットだと思います。

「上手くいっていたものを動作しないコードに書き換えてしまう」ことが頻発してしまいそうなので、それをリアルタイムで検知してくれるのは非常に頼もしく、安心してリファクタリングできそうです。

先にまとめ

テストの種類 テスト対象 テスト方法 テストを記載するファイル
単体テスト モデル モデルに記載された機能に対して、テストデータを投入して結果を確認する test/models
機能テスト コントローラ コントローラに対してHTTPリクエストを発行し、コントローラの振る舞いを確認する test/controllers
統合テスト モデル・コントローラ・ビュー 想定されるユーザの操作フローを模倣して、モデル・コントローラ・ビューの一連の振る舞いを確認する test/integration

参考:Rails チュートリアル 【初心者向け】 テストを10分でおさらいしよう!

以下でそれぞれのテストについて記載していきます。

単体テスト

単体テストはモデル(/app/models/xxx.rb)単位に記述します。
モデルに記載したバリデーションチェック等の機能一つに対して、対応するテストを一つ記述するイメージです。

例)

models/user.rb
class User < ApplicationRecord
  validates :name,  presence: true, length: { maximum: 50 }
end
test/models/user_test.rb
  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)を送り、
それに対するコントローラの振る舞いが想定通りであるかどうかを確認します。

例)

controllers/users_controller.rb
  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
test/controllers/users_controller_test.rb
  #有効なユーザ情報を登録した場合の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テスト)

統合テストはユーザーの実際の操作を想定し、操作のフローに伴うアプリの挙動を確認します。
テストを書くにあたってアプリケーションでユーザがどのような操作をできるのかを洗い出す必要がありそうです。

例)

test/integration/site_layout_test.rb
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

上記の例では、「ユーザがホーム画面に遷移する」という操作を想定ししています。

  1. ルートパスにgetリクエストを送り、ホーム画面に遷移することを確認する
  2. 遷移したホーム画面にて、画面上に表示しているリンクが想定通りであることを確認する

前述までの単体テスト・機能テストでは死んでいるリンクなどを検知できません。
統合テストを書くことでビューのバグも検知できるため、自分でブラウザを開いて一個一個クリックする必要がなくなりそうです。

本当にあっている?

ここまで記載しましたが、自分で怪しいと思っている点が2点あります。

  1. 機能テストの例で出したものは本当に機能テストになっているか?
    機能テストの例としたcreateコントローラのテストは「ユーザのログイン」操作を模倣した統合テストとも捉えられます。
    統合テストを書こうとしても上述した機能テストの繰り返しのようになってしまう気がしました。
    ①userコントローラにgetリクエストを送り、アクセスできることのテスト
    ②userコントローラに有効なpostリクエストを送り、ユーザ登録できることのテスト
    ③userコントローラに無効なpostリクエストを送り、ユーザ登録できないことのテスト
    とリクエスト一発に対して何が帰ってくるかを細かく区切った一つ一つが機能テストになるでしょうか?

  2. 「テストを記載するファイル」について別にルールはないのでは?
    上記の「test/controllers」に記載しているテストは「test/integration」の中に記載しても動きます。
    極端に言えば全てのテストを同じファイルに記載しても、テストは実行可能なのではないでしょうか。
    Tutorialで分かりやすいフォルダ構成を決めているだけで、テストを書くファイルは実際には自由?

  ・・・有識者の方、ご指摘いただけると助かります。

最後に

RailsTutorialは出てきたテストをそのままコピーしていたので、
それぞれのテストの違いを理解しないまま進めてしまっていたので、今回整理できてよかったです。

誤っている記述などあれば、コメントにてご教示いただけると助かります。

0
0
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
0
0