Railsでテストを書く

今回はRailsで自動テスト(モデルテストとシステムテスト)を書く方法を学びました。

Railsのテストはminitest(ほとんどがtest-unitと同じ)がベースになっています。

なぜテストを書くのか?
  1. 品質管理:テストは、アプリケーションの品質を保証する重要な手段。テストを書くことで、アプリケーションの動作を継続的に監視し、問題があれば早期に発見できる。
  2. バグ修正:テストは、バグ修正に役立つ。バグが発生した場合、その原因を特定するのに時間がかかる。しかし、テストを書いていれば、1つのテストが失敗した場合にその原因を特定することができる。(プログラムが壊れると顧客や利用者に迷惑がかかる)
  3. 変更管理:アプリケーションが進化するにつれて、変更が必要になる場合がある。テストを書いていれば、変更がアプリケーション全体にどのような影響を与えるかを簡単に確認できる。
  4. ドキュメンテーション:テストは、アプリケーションのドキュメンテーションとしても役立つ。テストを書くことで、アプリケーションの動作と想定される振る舞いを文書化することができる。
  5. 手作業で動作確認するよりも、自動化した方が最終的な費用対効果が高いから。

なぜテストを書くの?(または書かないの?) 〜テストコードの7つの役割〜

学んだこと✏️

テストに何を書けばいいかの基本的な考え
  • modelとhelper

    • 自分がmodelに作成したメソッドに対し、最低一つ(正常系)を書く。
  • controller

    • 書かなくても良い。
  • modelのvalidationなど、railsの機能、gemの機能をテストしてしまわないようにする。(gemのテストはgemの中でそれぞれやってるので必要ない)

Railsアプリにテストを追加
Userクラスのモデルテストを作成

test/fixtures/users.yml

john:
  email: john@example.com
  encrypted_password: <%= Devise::Encryptor.digest(User, 'password') %>
  name: John

mike:
  email: mike@example.com
  encrypted_password: <%= Devise::Encryptor.digest(User, 'password') %>
  name: Mike

alice:
  email: alice@example.com
  encrypted_password: <%= Devise::Encryptor.digest(User, 'password') %>
  name: Alice

test/models/users_test.rb

require 'test_helper'

class UserTest < ActiveSupport::TestCase
  def setup
    @john = users(:john)
    @mike = users(:mike)
    @alice = users(:alice)
  end

  test 'following? should return true if user is being followed' do
    @john.follow(@mike)
    assert @john.following?(@mike)
  end

  test 'following? should return false if user is not being followed' do
    assert_not @john.following?(@alice)
  end

  test 'followed_by? should return true if user is being followed by another user' do
    @mike.follow(@john)
    assert @john.followed_by?(@mike)
  end

  test 'followed_by? should return false if user is not being followed by another user' do
    assert_not @john.followed_by?(@alice)
  end

  test 'unfollow should destroy a relationship' do
    @john.follow(@mike)
    assert @john.following?(@mike)

    @john.unfollow(@mike)
    assert_not @john.following?(@mike)
  end

  test 'name_or_email should return name if present' do
    @john.name = 'John'
    @john.email = 'john@example.com'

    assert_equal 'John', @john.name_or_email
  end

  test 'name_or_email should return email if name is not present' do
    @john.name = nil
    @john.email = 'john@example.com'

    assert_equal 'john@example.com', @john.name_or_email
  end
end
  • 内部実装は変更される可能性もあるため、なるべく外部に見える振る舞いを中心にテストする。
Reportクラスのモデルテストの作成

test/fixtures/reports.yml

john_report:
  title: John's Report
  content: This is John's report
  user: john

mike_report:
  title: Mike's Report
  content: This is Mike's report
  user: mike
  • 連番(report1:とか)は数が増えると脳内マッピングが追いつかなくなるため、「ジョンの日報」「マイクの日報」等、連番以外で特徴付けられる名前を付けた方が良い。(他のfixtureも同様)
  • created_atRailsが自動的に設定するもの。fixtureで勝手に決められていると、チーム開発時に他の開発者が戸惑う為書かない。

test/models/report_test.rb

require 'test_helper'

class ReportTest < ActiveSupport::TestCase
  def setup
    @john = users(:john)
    @john_report = reports(:john_report)
  end

  test 'editable? returns true if the target user is the owner of the report' do
    assert @john_report.editable?(@john)
    assert_not @john_report.editable?(users(:mike))
  end

  test 'created_on should return the date of the report creation' do
    assert_equal Time.current.to_date, @john_report.created_on
  end
end
システムテストの作成

test/system/reports_test.rb

require 'application_system_test_case'

class ReportsTest < ApplicationSystemTestCase
  setup do
    @report = reports(:john_report)

    visit root_url
    fill_in 'Eメール', with: 'john@example.com'
    fill_in 'パスワード', with: 'password'
    click_on 'ログイン'
  end

  test 'visiting the index' do
    visit reports_url
    assert_selector 'h1', text: '日報'
  end

  test 'creating a Report' do
    visit reports_url
    click_on '新規作成'

    fill_in 'タイトル', with: '本日の日報'
    fill_in '内容', with: 'Railsでテストを書く'
    click_on '登録する'

    assert_text '日報が作成されました。'
    assert_text '本日の日報'
    assert_text 'Railsでテストを書く'
  end

  test 'updating a Report' do
    visit reports_url
    click_link '詳細', match: :first

    assert_text "John's Report"
    assert_text "This is John's report"

    click_on '編集', match: :prefer_exact
    fill_in 'タイトル', with: 'ジョンのレポート'
    fill_in '内容', with: 'これはジョンのレポートです'

    click_on '更新する'
    assert_text '日報が更新されました。'
    assert_text 'ジョンのレポート'
    assert_text 'これはジョンのレポートです'
  end

  test 'destroying a Report' do
    visit reports_url

    assert_text "John's Report"

    page.accept_confirm do
      click_on '削除', match: :first
    end

    assert_text '日報が削除されました。'
    assert_no_text "John's Report"
  end
end
  • 変更前は「あいうえお」だったが、変更後は「かきくけこ」に変わった、というようなテストの方が良い。 (変更前も「かきくけこ」だったら、ちゃんと変更できていない可能性があるため)
なぜcontrollerは書かなくても良い?
  • MVCの設計変化

    • RailsにおけるMVC(Model-View-Controller)の設計において、controllerは主にリクエストの処理やルーティングの制御を担う役割を持つ。しかし、近年のWebアプリケーションの設計トレンドでは、controllerが持つ役割をよりシンプルにし、ビジネスロジックをmodelやserviceに移す傾向がある。そのため、controllerの単体テストがあまり意味を持たなくなってきているとされている。
  • インテグレーションテストの重要性の向上

    • 複数のコンポーネント(例えば、controllerやmodel、viewなど)を組み合わせたテストを行うことで、システム全体の正常な動作を確認するものであり、controllerの単体テストよりもシステム全体の信頼性を高める効果があるとされている。
  • TDDの普及

    • テスト駆動開発(Test-Driven Development、TDD)の考え方が広まり、開発者がより多くのテストを書くようになった結果、controllerの単体テストが重要性を失ったとも言われている。TDDのアプローチでは、まずテストを書いてからコードを実装するため、controllerの単体テストよりも、より高いレベルのテストを書く傾向がある。
  • controllerの単体テストが完全に無視されるわけではない

    • 特定のケースや状況においては、controllerの単体テストが依然として有用であることもある。例えば、特定のロジックがcontrollerに集中している場合や、APIエンドポイントの動作確認をする場合などがある。開発現場の状況やプロジェクトの要件に応じて、適切なテスト戦略を選択することが重要。
参考