おもこん

おもこんは「思いつくままにコンピュターの話し」の省略形です

徒然Ruby(33)Rails7 システムテスト

Rails7におけるシステムテストについて書きます。

テンプレートの作成

コマンドラインからシステムテストのテンプレートを作成します。

$ bin/rails generate system_test words
    invoke test_unit
    create test/system/words_test.rb
$

/test/system/words_test.rbシステムテストを記述するファイルです。 ファイルを開くとWordsTestクラスの定義があります。 テストはその中に書いていきます。

require "application_system_test_case"

class WordsTest < ApplicationSystemTestCase
... ... ...
... ... ...
end

アサーション

クラスの親子関係は次のようになります。

WordsTest < ApplicationSystemTestCase < ActionDispatch::SystemTestCase < ActiveSupport::TestCase

このことから、アサーションについて次のことがわかります。

  • ActiveSupport::TestCaseがインクルードしたアサーションシステムテストでも使うことができる(以前の3つのテストと同様)
    • assert_changes
    • assert_difference
    • assert_no_changes
    • assert_no_difference
    • assert_not
    • assert_nothing_raised
  • 機能テストや結合テストではActionDispatch::IntegrationTestスーパークラスになっていたが、システムテストではスーパークラスではない。 ActionDispatch::IntegrationTestがインクルードしていたActionDispatch::Assertions::ResponseAssertionsActionDispatch::Assertions::RoutingAssertionsは、 システムテストではインクルードされないので、以下のアサーションは使えない。
    • assert_response
    • assert_redirect_to
    • assert_generates
    • assert_recognizes
    • assert_routing
  • ActionDispatch::SystemTestCaseCapybara::Minitest::Assertionsをインクルードしているので、Capybaraのアサーションを使うことができる。 このアサーションRailsAPIリファランスには説明がない。 CapybaraのAPIリファランスにその説明がある。 よく使いそうなアサーション(個人の独断による)を以下に示す。
    • assert_selector(セレクタ名, オプション): セレクタが送られてきたHTMLページ(以下ページと略記)にあるかどうかをチェック。 オプションでよく使うのは「text: 文字列または正規表現」「count: 整数」(そのセレクタの出現回数のチェック)「visible: (true|falseなど)」(可視かどうか)など。
    • assert_text(文字列, オプション): 文字列がページにあるかどうかをチェック。 オプションには「count: 整数」などが可。 デフォルトでは文字列は「可視文字列」、つまり改行やコントロールコードなどは含まない
    • assert_button(文字列): 引数がボタンのidまたは表示文字列であるようなボタンがあるかどうかのチェック
    • assert_field(文字列, オプション): 入力フィールドで引数の文字列にそのラベルまたは名前またはidが一致するものがあるかどうかをチェック。 オプションには「type: "textarea"」などの入力タイプの指定ができる
    • assert_link(文字列, オプション): リンクで、そのidまたはテキストが引数の文字列に一致するものがあるかどうかをチェック。 オプションにはリンク先「href: 文字列または正規表現」を指定できる
    • assert_table(文字列, オプション): 引数の文字列に一致するidまたはキャプションを持つ表があるかどうかをチェック。 オプションでは表の内容のチェックができるが、詳しくはAPIリファランスを参照してほしい
    • assert_title(文字列): 文字列に一致するタイトルを持っているかどうかをチェック

良く使われるメソッド

結合テストで使えたgetメソッドなどはシステムテストでは使えません。 代わりにCapybaraのメソッドを使います。 良く使われると思われるメソッドを以下に簡単に説明します。 その詳細や、他のメソッドについてはCapybaraのドキュメントを参照してください。

  • visit(URLまたはパス): URLへのGETメソッドでのアクセスをシミュレート
  • fill_in(フィールド名, with: 文字列): フォーム・フィールドに文字列をセットする
  • click_link(テキスト): リンクのクリックをシミュレート。テキストはそのaタグのコンテンツ(開始タグと終了タグで挟まれた部分)
  • click_button(テキスト): ボタンのクリックをシミュレート。テキストはそのボタンに表示される文字列
  • click_on(テキスト): リンクまたはボタンのクリックをシミュレート。click_linkclick_buttonの両方をまとめたメソッド
  • accept_confirm ブロック: ブロック(多くはリンクやボタンのクリック)を実行したときに、確認ダイアログが現れる場合、確認を受け入れる(OKをクリックなど)
  • dismiss_confirm ブロック: ブロック(多くはリンクやボタンのクリック)を実行したときに、確認ダイアログが現れる場合、確認を拒否する(NOやCancelをクリックなど)

最後の2つは、例えば削除に対する「are you sure?」などの確認ダイアログが出る場合に必要なメソッドです。 click_onだけではエラーになります。

ここまでで説明したメソッドは、ユーザがクリックやキーボード入力するのをシミュレートします。 テストでは、ユーザの行動をシミュレートしますので、現実の運用に近い形でのテストになります。

システムテストの例

ここでは、ルートにアクセスしてから、検索、追加、変更、削除を一通り行うことをシミュレートしてテストします。 プログラムリストにコメントしてありますが、次の順に推移することを想定しています。

  • ルートにアクセス
  • 正規表現「.」で検索
  • 単語を追加
  • その単語を変更
  • その単語を削除
  • index画面に戻る
require "application_system_test_case"

class WordsTest < ApplicationSystemTestCase
  test "flow from the root through every action" do
    # ルートにアクセス=>indexアクションへ
    visit root_url
    assert_selector "h1", text: "単語帳"

    # searchアクションへ
    fill_in "search", with: "."
    click_button "検索"

    assert_selector "h1", text: "単語検索結果"
    ["tall","高い","house",""].each do |s|
      assert_text s
    end

    # newアクションへ
    click_link "追加"

    fill_in "word[en]", with: "stop"
    fill_in "word[jp]", with: "止まる"
    fill_in "word[note]", with: "The machine stopped.\n機械が止まった。\n"
    wc = Word.count

    # createアクションへ
    click_button "作成"

    # showアクションへリダイレクト
    assert_text "単語を保存しました"
    assert_equal 1, Word.count-wc
    ["stop","止まる","The machine stopped.","機械が止まった。"].each do |s|
      assert_text s
    end

    # editアクションへ
    click_link "変更"

    fill_in "word[jp]", with: "止める"
    fill_in "word[note]", with: "I stopped speaking.\n私は話すのをやめた。\n"

    # updateアクションへ
    click_button "変更"

    # showアクションへリダイレクト
    ["stop","止める","I stopped speaking.","私は話すのをやめた。"].each do |s|
      assert_text s
    end
    wc = Word.count

    # deleteアクションへ
    accept_confirm do
      click_link "削除"
    end

    # indexアクションに戻る
    assert_text "単語を削除しました"
    assert_equal -1, Word.count-wc
    assert_selector "h1", text: "単語帳"
  end
end

プログラムを読んでもらえば、ユーザがクリックしたりキーボード入力するのをそのままシミュレートしていることが分かると思います。 いくつかポイントになる点を書きます。

assert_textでは可視文字列のみをチェックする(デフォルト動作)ので、改行をチェックすることはできません。 2行にわたる文字列は、2回に分けて1行ずつチェックします。

createアクションでデータベースの単語数が増えるのをassert_equalでチェックしました。 assert_differenceでも可能ですが、そのブロック内にデータベースに書き込むタイミングを含まなければならないことに注意が必要です。 次のプログラムではフェイルする可能性が高いです。

assert_difference("Word.count") do
  click_button "作成"
end
assert_text "単語を保存しました"

これは、ボタンクリック直後ではまだデータベースの書き込みが終わっていないため、Word.countがブロックの前後で変化していないためです。 これは次のように書くことで解決できます。

assert_difference("Word.count") do
  click_button "作成"
  assert_text "単語を保存しました"
end

assert_textが画面が変わるまで待ってからチェックをしてくれるので、ブロック実行中にデータベース書き込みは行われています。 Capybaraでは、このようにアサーションが画面遷移を待つようになっているので、それに注意が必要です。 assert_differenceを使うことは可能ですが、どちらかといえばassert_equalを使うほうが分かりやすいように思います。

削除メニューをクリックすると確認ダイアログが現れます。 Capybaraではこのようにダイアログが現れる場合はaccept_confermのブロックにリンクやボタンのクリックを書かなければなりません。 そうでないとエラーになります。

もし、ダイアログでCancelやNoボタンを押すことをシミュレートしたい場合はdismiss_confirmを使います。

テストの実行

テストの実行にはtest:systemを引数にbin/railsを実行します。

$ bin/rails test:system
yarn install v1.22.19
[1/4] Resolving packages...
success Already up-to-date.
Done in 0.11s.
yarn run v1.22.19
$ esbuild app/javascript/*.* --bundle --sourcemap --outdir=app/assets/builds --public-path=assets

  app/assets/builds/application.js      394.1kb
  app/assets/builds/application.js.map  721.7kb

Done in 0.20s.
yarn install v1.22.19
[1/4] Resolving packages...
success Already up-to-date.
Done in 0.10s.
yarn run v1.22.19
$ sass ./app/assets/stylesheets/application.bootstrap.scss:./app/assets/builds/application.css --no-source-map --load-path=node_modules
Done in 1.86s.
Running 1 tests in a single process (parallelization threshold is 50)
Run options: --seed 20243

# Running:

DEBUGGER: Attaching after process 8081 fork to child process 8111
Capybara starting Puma...
* Version 5.6.5 , codename: Birdie's Version
* Min threads: 0, max threads: 4
* Listening on http://127.0.0.1:38767
.

Finished in 3.190794s, 0.3134 runs/s, 5.9546 assertions/s.
1 runs, 19 assertions, 0 failures, 0 errors, 0 skips

多くのメッセージが出ていますが、その多くはテストの準備作業です。 テスト自体は下から14行目の「Running 1 tests in a single process (parallelization threshold is 50)」以下になります。 下から4行目のドットがテストの成功を示しています。 さらに、その詳細は、最終行のテスト1、アサーション19、フェイルとエラーは0だったことが分かります。

ヘッドレスブラウザ

システムテストではブラウザ画面が表示されます。 これは画面を見ることができて良いともいえますが、逆に煩わしいともいえます。 画面表示の部分を無くしたヘッドレスブラウザを使うことでこの問題を解決できます。

設定についてはRails Guideを参照してください。

まとめ

この記事の例から分かると思いますが、システムテストでは実際の運用に近い形でのテストができます。 本当にシミュレーションという感じです。 演劇でいえば舞台稽古、コンサートでいえばリハーサルのようなものです。

したがって、Railsウェブアプリケーションは最終的にはシステムテストを書かなければならないと思います。 そうであれば、結合テストは省略される可能性もあるでしょう。 これはそれぞれのプロジェクトでの考え方になると思います。

今回でRailsの記事が7本になりました。 ここでRailsは終わりになります。 次回以降は別のトピックを取り上げたいと思います。