【Rails7】単体テストと統合テストを行う手順

Rails で実装した機能がうまく動作するか、もしくは無効な値が入力された場合にちゃんとエラーを吐き出してくれるかどうか(バリデーションが効いているか)。

これらをシステム的に確認してくれるのがテスト。

ただ、ぶっちゃけた話、アプリの実装に加えてテストのコードまで書くのは正直面倒くさいので、ブラウザ上でちゃんと機能するか確認すれば良くね?

って思っていました、最初はw

ただ、アプリの規模が大きくなってきた時に、変更やアップデートを行うたびにブラウザ上でいちいち動作確認をするのはもっと面倒だよな、

と思い始めて、それなら最初からテストコード書いておいた方が後々楽になるだろう、だから今のうちにテストコードについて学んでおこう、

それにもし今後本格的なWebサービスを作るなら、致命的な不具合を出さないためにもテストコードは必須になるよな、、

Qiita
【初心者向け】テストコードの方針を考える(何をテストすべきか?どんなテストを書くべきか?) - Qiita はじめに 「テストコードを書きましょう」とはよく言われるし、テストコードが大事だってことも理解できるんだけど、何をテストしたらいいの?どんなテストを書いたらいい...

ということで、今回はRailsでテスト(単体テスト、統合テスト)を行う手順について学んだことを、今後のためにもメモしておこうと思います。

目次

開発環境

  • Ruby 3.1.2
  • Ruby on Rails 7.0.3
  • M1 Macbook Air 2020
  • mac OS Monterey (ver. 12.4)
  • ターミナル bash (Rosetta 2 使用)

ソースコード

GitHub
GitHub - hirokirokki0820/user-authentication Contribute to hirokirokki0820/user-authentication development by creating an account on GitHub.

単体テストと統合テスト

Rails のテストは主に2種類に分類され、一つは「単体テスト」、もう一つは「統合テスト」と呼ばれています。

「単体テスト」は一つの機能に関して正常に動くかどうかを見るテストのことで、

モデルのバリデーションや、コントローラーの動作を一つ一つ確認していく時には、この単体テストを行います。

「統合テスト」は、複数の機能が連動して行われる処理が正常に動くかどうかを見るテストのことで、

例えばユーザーがログインしてからログアウトするまでの一連の流れがうまく機能するかどうかをみたい時に、この統合テストを行います。

TechTechMedia
【Rails】プログラミング開発におけるテストコードの意義について初心者向けに解説|TechTechMedia プログラミング開発におけるテストコードの意義について簡単に解説しています。テストコードのメリット・デメリットやその必要性、Railsにおけるテスティングフレームワー...
Rails テスティングガイド - Rails...
Rails テスティングガイド - Railsガイド Railsでさまざまなテストを実行するための総合的な解説を行います。「テスティングとは何か」から結合テストまですべてのトピックを扱います。

ちなみに、テストコードを書く際に用いられるテスティングフレームワークは色々ありますが、ここではRailsにデフォルトで入っているminitestを用いて話を進めていきます。

(最も有名かつポピュラーなテスティングフレームワークはRSpecだそうです。僕もいずれはRSpecに移行しようと思っています)

単体テストを行う流れ

Rails は以下のコマンドでモデルを作成すると、自動的にモデルのテスト用ファイルも作成してくれます。

$ rails g model User name:string email:string password_digest:string
...
create  app/models/user.rb
create  test/models/user_test.rb
create  test/fixtures/users.yml
...

コントローラーを作成した場合は、コントローラー用のテストファイルが自動生成されます。

$ rails scaffoldした場合は、モデル用、コントローラー用の両方のテストファイルが生成されます)

ここでは、Userモデルを作成したというていで話を進めていきましょう。

Userモデルではnameやemail、passwordに対してさまざまなバリデーションを効かせると思いますが、

これらのバリデーションがちゃんと効いているかどうかを一つ一つ確認していきます。

Userモデルのテストを行うためには、test/modelsディレクトリ内のuser_test.rbにテストコードを記述していきます。

例えば、Userモデルのnameの値を必須にしたい(空にしたらエラーが出るようにしたい)場合は、以下のようなテストコードを記述してテストします。

require "test_helper"

class UserTest < ActiveSupport::TestCase
  def setup
    @user = User.new(name: "hiroking",
                    email: "hiroking@example.com",
                    password: "hiroking",
                    password_confirmation: "hiroking"
                    )
  end

  test "name should be present" do
    @user.name = " "
    assert_not @user.valid?
  end

end

“name should be present” と命名されたテストでは、

ユーザー名がnilのときに( @user.name = ” “)、assert_not で @user.valid? が false になってくれればテスト成功となります。

assert_notはアサーションの一つで、オブジェクトまたは式を評価して、期待された結果が得られるかどうかをチェックするコードのことです。

利用可能なアサーション一覧はRailsの公式ドキュメントを参照。

続いて、モデルの単体テストを実行する場合は、以下のコマンド実行します。

$ rails test:models

先ほどのテストコード “name should be present” でテストがパスしなかった場合は以下のようにFailure:と表示されます。

# Running:

......F

Failure:
UserTest#test_name_should_be_present [/Users/hirokirokki0820/Desktop/Ruby_on_Rails/user-authentication/test/models/user_test.rb:18]:
Expected true to be nil or false


rails test test/models/user_test.rb:16

この「Expected true to be nil or false」という文面から、

nameカラムがnilの時の期待値はfalse になるはずなのにtrueになってるよ、ということが読み取れます。

(nameが空っぽなのに、ユーザー登録できちゃってるよ?という意味)

この結果から、Userモデルのnameカラムにバリデーションが効いていないことが発覚しました。

バリデーションを効かせて、もう一度実行するとテストはパスするはずです。

ターミナルに以下のような表示が出たら、テストがパスした証拠です。

Running 15 tests in a single process (parallelization threshold is 50)
Run options: --seed 15386

# Running:

...............

Finished in 3.174605s, 4.7250 runs/s, 8.5050 assertions/s.
15 runs, 27 assertions, 0 failures, 0 errors, 0 skips

今回はnameカラムを例に挙げましたが、他にもemail, passwordカラムなど、各カラムごとに必要なテストコードを追記し、一つ一つ機能するか確認していきます。

一応、今回Userモデル(name, email, password)に適用したバリデーション、およびバリデーションチェックで必要そうなテストコードのサンプルを置いておきます。

class User < ApplicationRecord
  # email オブジェクトが保存される時点で小文字に変換する
  before_save { self.email = email.downcase }

  # name のバリデーション
  validates :name, presence: true, length: { maximum: 25 }

  # email のバリデーション
  VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i
  validates :email, presence: true,
                    uniqueness: { case_sensitive: false },
                    length: { maximum: 105 },
                    format: { with: VALID_EMAIL_REGEX }
  
  # password のバリデーション
  has_secure_password
  validates :password, presence: true, length: { minimum: 6 }
  validates :password_confirmation, presence: true
end
require "test_helper"

class UserTest < ActiveSupport::TestCase
  def setup
    @user = User.new(name: "hiroking",
                    email: "hiroking@example.com",
                    password: "hiroking",
                    password_confirmation: "hiroking"
                    )
  end

  test "should be valid" do
    assert @user.valid?
  end

  test "name should be present" do
    @user.name = " "
    assert_not @user.valid?
  end

  test "email should be present" do
    @user.email = " "
    assert_not @user.valid?
  end

  test "name should not be too long" do
    @user.name = "a" * 100
    assert_not @user.valid?
  end

  test "email should not be too long" do
    @user.email = "a" * 255 + "example.com"
    assert_not @user.valid?
  end

  test "email validation should accept valid addresses" do
    invalid_addresses = %w[user@example,com user_at_foo.org user.name@example.
    foo@bar_baz.com foo@bar+baz.com]
    invalid_addresses.each do |invalid_address|
      @user.email = invalid_address
      assert_not @user.valid?, "#{invalid_address.inspect} should be invalid"
    end
  end

  test "email addresses should be unique" do
    duplicate_user = @user.dup
    duplicate_user.email = @user.email.upcase
    @user.save
    assert_not duplicate_user.valid?
  end

  test "email addresses should be saved as lower-case" do
    mixed_case_email = "Foo@ExAMPle.CoM"
    @user.email = mixed_case_email
    @user.save
    assert_equal mixed_case_email.downcase, @user.email
  end

  test "password should be present (nonblank)" do
    @user.password = @user.password_confirmation = " " * 6
    assert_not @user.valid?
  end

  test "password should have a minimum length" do
    @user.password = @user.password_confirmation = "a" * 5
    assert_not @user.valid?
  end

  test "password should be confirmed" do
    @user.password = "a" * 10
    @user.password_confirmation = "b"* 10
    assert_not @user.valid?
  end


end

統合テストを行う流れ

今回はユーザーの新規登録の一連の流れをテストするというていで話を進めます。

以下のコマンドを実行し、test/integrationディレクトリにテスト用のファイルを作成します。

$ rails g integration_test (テスト名)

(テスト用のファイルは(テスト名)_test.rbという名前で出力される)

今回はユーザーの新規登録のテストということで、テスト名はusers_signupとしました。

test/integrationフォルダ内にはusers_signup_test.rbが作成されます。

require "test_helper"

class UsersSignupTest < ActionDispatch::IntegrationTest
# この中にテストコードを記述する
end

テストコードは上記の UsersSignupTestクラス内に書いていきます。

では、まずユーザー登録時に無効な値を入れるとユーザーが作成されない(登録に失敗する)ことを確認するテストを記述していきましょう。

require "test_helper"

class UsersSignupTest < ActionDispatch::IntegrationTest

  # 新規ユーザー登録用の統合テスト
  # ユーザー登録時に(ユーザー情報が無効であるために)ユーザーが作成されないことを確認する
  test "invalid signup information" do
    get new_user_path
    # 期待値:ユーザー数の合計が変化しない(ユーザー登録に失敗)
    assert_no_difference 'User.count' do
      # users_pathに対してポストリクエストを送信する
      post users_path, params:
                      { user: { name: "",
                              email: "user@invalid",
                              password: "foo",
                              password_confirmation: "bar" } }
    end
    # エラー発生時に new_user_path をレンダリングしているかどうか
    assert_template "users/new"
  end

end

テストコードを書いたら、ターミナルで以下のコマンドを実行してテストを行います。

$ rails test

上記のテストコードでは、

  1. ユーザーが新規登録フォームにアクセスする
  2. 登録情報に無効な値を入力する(空にする、無効なメアド入力、パスワードの不一致など)
  3. 登録に失敗したら、新規登録フォームをレンダリングする

という一連の流れが実行できているかどうかをチェックしています。

ユーザーの登録に失敗しているかどうかは、テストコード内の

assert_no_difference 'User.count' do

この部分で、データーベース内のユーザー数の値(カウント数)が増えていないかどうかで判断しています。

ユーザー登録に失敗していたらユーザー数の合計は増えないはずですからね。

続いては、ユーザー登録時に有効な値を入れるとユーザーが作成される(登録に成功する)ことを確認するテストを記述していきましょう。

先ほどのテストの下につづけて、以下のように記述します。

require "test_helper"

class UsersSignupTest < ActionDispatch::IntegrationTest
  # 新規ユーザー登録用の統合テスト
  # ユーザー登録時に(ユーザー情報が無効であるために)ユーザーが作成されないことを確認する
  test "invalid signup information" do
     
  (省略)
   
  end

  # ユーザー登録時にユーザーが作成されることを確認する
  test "valid signup information" do
    get new_user_path
    # 期待値:ユーザー数の合計が+1される(ユーザー登録に成功したのと同意)
    assert_difference 'User.count', 1 do
      post users_path, params: { user:
                    { name: 'test user',
                    email: 'test@example.com',
                    password: 'password',
                    password_confirmation: 'password' } }
    end
    assert_response :redirect
    #リダイレクト実行後に続いて別のリクエストを行う予定がある場合は、follow_redirect!を呼び出す
    follow_redirect!
    assert_response :success
    assert_template "users/show"
    # showページの指定タグ内にユーザー名が表示されているかを確認
    assert_select "p.header", "test user"
    assert_select "div.ui.card>.content>.header", "test user"
    # showページにフラッシュメッセージが表示されているかを確認
    assert_select "div.ui.message.success"
  end

end

今回はユーザー登録の一連の流れをテストするコードのサンプルを挙げましたが、

ログインやログアウト、記事の投稿や削除、もしくはその全ての一連の流れをテストする場合は、

別途ファイルを作成して上記のような感じでテストコードを書いていきます。

以上、テストコード作成の流れでした。

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!

この記事を書いた人

愛知の34歳。ブロガー&フリーター(現在無職)。院卒で大手自動車部品メーカーに入社するも、仕事が嫌になり3年で退職。28歳でNZワーホリへ。帰国後から現在まで、ワーホリのような「働きたい時だけ働く」「嫌なことはしない」という生き方しています。ここ1年は無職。趣味は登山、旅、音ゲー(ギタフリ)、たまにプログラミング。

コメント

コメントする

目次