Ruby
minitest
プロを目指す人のためのRuby入門

「プロを目指す人のためのRuby入門」でテスト失敗時に実行結果が正常に表示されない場合

はじめに

Teratailにて、書籍「プロを目指す人のためのRuby入門」に関連する以下のような質問が投稿されていました。

Ruby - minitestでテストが失敗したとき1 runs, 2 assertions, 1 failures...というような表示が出ない(110066)|teratail

どうも、gemのインストール状況によってはMinitestの不具合を踏み抜いてしまうようです。

この記事では発生する問題とその解決策を説明します。

発生する問題

「プロを目指す人のためのRuby入門」に書いてあるとおりにテストコードを入力し、わざとテストを失敗させると、本のような出力結果にならない(スタックトレースが表示されてしまう)。

例:3.2.4項のテストコード

require 'minitest/autorun'

class SampleTest < Minitest::Test
  def test_sample
    # わざとcapitalizeメソッド(最初の1文字だけを大文字にするメソッド)を呼ぶ
    assert_equal 'RUBY', 'ruby'.capitalize
  end
end

本に書いてある出力結果(正しい)

$ ruby sample_test.rb
Run options: --seed 14255

# Running:

F

Finished in 0.001383s, 723.1400 runs/s, 723.1400 assertions/s.

  1) Failure:
SampleTest#test_sample [sample_test.rb:5]:
Expected: "RUBY"
  Actual: "Ruby"

1 runs, 1 assertions, 1 failures, 0 errors, 0 skips

エラーが出て最後まで表示されない場合(おかしい)

$ ruby sample_test.rb
Run options: --seed 14255

# Running:

F

Finished in 0.001383s, 723.1400 runs/s, 723.1400 assertions/s.

  1) Failure:
SampleTest#test_sample [sample_test.rb:5]:
Expected: "RUBY"
  Actual: "Ruby"


/Users/jit/.rbenv/versions/2.4.3/lib/ruby/gems/2.4.0/gems/railties-5.1.4/lib/rails/test_unit/reporter.rb:70:in `method': undefined method `test_sample' for class `Minitest::Result' (NameError)
    from /Users/jit/.rbenv/versions/2.4.3/lib/ruby/gems/2.4.0/gems/railties-5.1.4/lib/rails/test_unit/reporter.rb:70:in `format_rerun_snippet'
    from /Users/jit/.rbenv/versions/2.4.3/lib/ruby/gems/2.4.0/gems/railties-5.1.4/lib/rails/test_unit/reporter.rb:23:in `record'
    from /Users/jit/.rbenv/versions/2.4.3/lib/ruby/gems/2.4.0/gems/minitest-5.11.1/lib/minitest.rb:786:in `block in record'
    from /Users/jit/.rbenv/versions/2.4.3/lib/ruby/gems/2.4.0/gems/minitest-5.11.1/lib/minitest.rb:785:in `each'
    from /Users/jit/.rbenv/versions/2.4.3/lib/ruby/gems/2.4.0/gems/minitest-5.11.1/lib/minitest.rb:785:in `record'
    from /Users/jit/.rbenv/versions/2.4.3/lib/ruby/gems/2.4.0/gems/minitest-5.11.1/lib/minitest.rb:334:in `run_one_method'
    from /Users/jit/.rbenv/versions/2.4.3/lib/ruby/gems/2.4.0/gems/minitest-5.11.1/lib/minitest.rb:321:in `block (2 levels) in run'
    from /Users/jit/.rbenv/versions/2.4.3/lib/ruby/gems/2.4.0/gems/minitest-5.11.1/lib/minitest.rb:320:in `each'
    from /Users/jit/.rbenv/versions/2.4.3/lib/ruby/gems/2.4.0/gems/minitest-5.11.1/lib/minitest.rb:320:in `block in run'
    from /Users/jit/.rbenv/versions/2.4.3/lib/ruby/gems/2.4.0/gems/minitest-5.11.1/lib/minitest.rb:360:in `on_signal'
    from /Users/jit/.rbenv/versions/2.4.3/lib/ruby/gems/2.4.0/gems/minitest-5.11.1/lib/minitest.rb:347:in `with_info_handler'
    from /Users/jit/.rbenv/versions/2.4.3/lib/ruby/gems/2.4.0/gems/minitest-5.11.1/lib/minitest.rb:319:in `run'
    from /Users/jit/.rbenv/versions/2.4.3/lib/ruby/gems/2.4.0/gems/minitest-5.11.1/lib/minitest.rb:159:in `block in __run'
    from /Users/jit/.rbenv/versions/2.4.3/lib/ruby/gems/2.4.0/gems/minitest-5.11.1/lib/minitest.rb:159:in `map'
    from /Users/jit/.rbenv/versions/2.4.3/lib/ruby/gems/2.4.0/gems/minitest-5.11.1/lib/minitest.rb:159:in `__run'
    from /Users/jit/.rbenv/versions/2.4.3/lib/ruby/gems/2.4.0/gems/minitest-5.11.1/lib/minitest.rb:136:in `run'
    from /Users/jit/.rbenv/versions/2.4.3/lib/ruby/gems/2.4.0/gems/minitest-5.11.1/lib/minitest.rb:63:in `block in autorun'

この問題が発生する条件

以下の条件が重なるとこの問題が発生するようです。

  • バージョン5.1.4以下のRailsがインストールされている(5.0系は大丈夫かも)
  • バージョン5.11.0以上のMinitestがインストールされている

「Minitest 5.11.0なんてインストールした記憶がないよ!」と思う人もいるかもしれませんが、おそらくRailsアプリケーションを作成したタイミングで、知らないうちにインストールされてるんだと思います。

ちなみに、次のようなコマンドを打つと、ローカル環境にインストールされているgemとバージョンを確認できます。

$ gem list | grep minitest
minitest (5.11.1, 5.10.3, 5.10.1)

$ gem list | grep rails
rails (5.1.4)

Windowsの場合はgrepコマンドの代わりにfindコマンドを使ってください。

> gem list | find "minitest"
minitest (5.11.1, 5.10.3, 5.10.1)

> gem list | find "rails"
rails (5.1.4)

この問題の原因

この問題の原因は以下のとおりです。

  • Minitestはデフォルトでローカルにインストールされているgemから、minitest/*_plugin.rbという名前のファイルをプラグインとして自動的に読み込む(参考)。
  • Railsがインストールされている場合、Railsにrailties/lib/minitest/rails_plugin.rbというファイルがあるため、このファイルが読み込まれる(参考)。
  • バージョン5.1.4以下のRailsではMinitest 5.11.0以降の仕様変更に対応していないため、テスト失敗時にエラーが発生する(参考)。

解決策

Rails 5.1.5でこの問題が解決しているので、最新のRailsをインストールすれば解決します。

$ gem install rails

gemをインストールしたら以下のコマンドを実行し、バージョンが一番新しいRailsが5.1.5以上になったことを確認してください。

# Mac/Linuxの場合
$ gem list | grep rails
rails (5.1.5, 5.1.4)

# Windowsの場合
> gem list | find "rails"
rails (5.1.5, 5.1.4)

参考:2018.01.26時点の解決策

Rails側の対応はこちらのコミットで対応されていますが(参考)、2018年1月26日時点ではまだ新しいバージョンのRailsがリリースされていません。

ですので、ここでは--no-pluginsオプションを付けて、プラグインを自動的に読み込まないようにして実行する方法を解決策として紹介します。

先ほどのコマンドの後ろに--no-pluginsを付けて実行すると、Railsのプラグインが読み込まれないため、正常にテストが終了します。

$ ruby sample_test.rb --no-plugins
Run options: --seed 14255

# Running:

F

Finished in 0.001383s, 723.1400 runs/s, 723.1400 assertions/s.

  1) Failure:
SampleTest#test_sample [sample_test.rb:5]:
Expected: "RUBY"
  Actual: "Ruby"

1 runs, 1 assertions, 1 failures, 0 errors, 0 skips

rakeコマンドで実行する場合(12.7.2項)

12.7.2項ではrakeコマンドでテストを一括実行する方法を紹介しています。
本の中では全件がパスする例しか載せていませんが、テストが失敗する場合は上で説明したものと同じエラーが発生します。

$ rake
Run options: --seed 30052

# Running:

..............F

Failure:
RgbTest#test_to_hex [/Users/jit/dev/ruby-book-codes/ruby-book/test/rgb_test.rb:7]:
Expected: "#fffff"
  Actual: "#ffffff"


/Users/jit/.rbenv/versions/2.4.3/lib/ruby/gems/2.4.0/gems/railties-5.1.4/lib/rails/test_unit/reporter.rb:70:in `method': undefined method `test_to_hex' for class `Minitest::Result' (NameError)
    from /Users/jit/.rbenv/versions/2.4.3/lib/ruby/gems/2.4.0/gems/railties-5.1.4/lib/rails/test_unit/reporter.rb:70:in `format_rerun_snippet'
    from /Users/jit/.rbenv/versions/2.4.3/lib/ruby/gems/2.4.0/gems/railties-5.1.4/lib/rails/test_unit/reporter.rb:23:in `record'
    from /Users/jit/.rbenv/versions/2.4.3/lib/ruby/gems/2.4.0/gems/minitest-5.11.2/lib/minitest.rb:803:in `block in record'
    from /Users/jit/.rbenv/versions/2.4.3/lib/ruby/gems/2.4.0/gems/minitest-5.11.2/lib/minitest.rb:802:in `each'
    from /Users/jit/.rbenv/versions/2.4.3/lib/ruby/gems/2.4.0/gems/minitest-5.11.2/lib/minitest.rb:802:in `record'
    from /Users/jit/.rbenv/versions/2.4.3/lib/ruby/gems/2.4.0/gems/minitest-5.11.2/lib/minitest.rb:334:in `run_one_method'
    from /Users/jit/.rbenv/versions/2.4.3/lib/ruby/gems/2.4.0/gems/minitest-5.11.2/lib/minitest.rb:321:in `block (2 levels) in run'
    from /Users/jit/.rbenv/versions/2.4.3/lib/ruby/gems/2.4.0/gems/minitest-5.11.2/lib/minitest.rb:320:in `each'
    from /Users/jit/.rbenv/versions/2.4.3/lib/ruby/gems/2.4.0/gems/minitest-5.11.2/lib/minitest.rb:320:in `block in run'
    from /Users/jit/.rbenv/versions/2.4.3/lib/ruby/gems/2.4.0/gems/minitest-5.11.2/lib/minitest.rb:360:in `on_signal'
    from /Users/jit/.rbenv/versions/2.4.3/lib/ruby/gems/2.4.0/gems/minitest-5.11.2/lib/minitest.rb:347:in `with_info_handler'
    from /Users/jit/.rbenv/versions/2.4.3/lib/ruby/gems/2.4.0/gems/minitest-5.11.2/lib/minitest.rb:319:in `run'
    from /Users/jit/.rbenv/versions/2.4.3/lib/ruby/gems/2.4.0/gems/minitest-5.11.2/lib/minitest.rb:159:in `block in __run'
    from /Users/jit/.rbenv/versions/2.4.3/lib/ruby/gems/2.4.0/gems/minitest-5.11.2/lib/minitest.rb:159:in `map'
    from /Users/jit/.rbenv/versions/2.4.3/lib/ruby/gems/2.4.0/gems/minitest-5.11.2/lib/minitest.rb:159:in `__run'
    from /Users/jit/.rbenv/versions/2.4.3/lib/ruby/gems/2.4.0/gems/minitest-5.11.2/lib/minitest.rb:136:in `run'
    from /Users/jit/.rbenv/versions/2.4.3/lib/ruby/gems/2.4.0/gems/minitest-5.11.2/lib/minitest.rb:63:in `block in autorun'
rake aborted!
Command failed with status (1)

Tasks: TOP => default => test
(See full trace by running task with --trace)

rakeコマンドで実行する場合は、次のように手前にMT_NO_PLUGINS=1を付けて実行してください。

$ MT_NO_PLUGINS=1 rake
Run options: --seed 40412

# Running:

...........F.....

Finished in 0.003018s, 5632.8694 runs/s, 11265.7388 assertions/s.

  1) Failure:
RgbTest#test_to_hex [/Users/jit/dev/ruby-book-codes/ruby-book/test/rgb_test.rb:7]:
Expected: "#fffff"
  Actual: "#ffffff"

17 runs, 34 assertions, 1 failures, 0 errors, 0 skips
rake aborted!
Command failed with status (1)

Tasks: TOP => default => test
(See full trace by running task with --trace)

Windows環境の場合は先にsetコマンドでMT_NO_PLUGINS=1の環境変数を設定してからrakeコマンドを実行してください。

> set MT_NO_PLUGINS=1
> rake

なお、環境変数については12.5節でも説明しています。

参考:2018.01.22時点の解決策

以下はこの記事を改訂する前に載せていた、Bundlerを経由して実行する解決策です。
この方法でも解決しますが、--no-pluginsオプションを付けて実行する方が手軽だと思います。

参考までに手順を紹介します。

1. Bundlerをインストールする

gemコマンドでインストールします(Rails環境を構築している場合は、すでにインストール済みかもしれません)。

$ gem install bundler

2. Gemfileを作成する

bundle initコマンドで作成できます。

$ bundle init
Writing new Gemfile to /Users/jit/dev/ruby-book-codes/ruby-book/Gemfile

3. Gemfileを以下のように編集する

「プロを目指す人のためのRuby入門」ではMinitest 5.10.1で動作確認しています。

 # frozen_string_literal: true

 source "https://rubygems.org"

 git_source(:github) {|repo_name| "https://github.com/#{repo_name}" }

-# gem "rails"
+gem 'minitest', '5.10.1'

4. gemをインストールする

bundle installコマンドでgemをインストールします。

$ bundle install
Fetching gem metadata from https://rubygems.org/.............
Resolving dependencies...
Using bundler 1.16.1
Using minitest 5.10.1
Bundle complete! 1 Gemfile dependency, 2 gems now installed.
Use `bundle info [gemname]` to see where a bundled gem is installed.

5. bundle execを付けてrubyコマンドを実行する

rubyコマンドの手前にbundle execを付けて実行すると、本と同じ出力結果が得られるはずです。

$ bundle exec ruby sample_test.rb 
Run options: --seed 62637

# Running:

F

Finished in 0.000750s, 1333.3334 runs/s, 1333.3334 assertions/s.

  1) Failure:
SampleTest#test_sample [./chapter_03/code_3.02.01.rb:6]:
Expected: "RUBY"
  Actual: "Ruby"

1 runs, 1 assertions, 1 failures, 0 errors, 0 skips

「なぜこれでうまく動くようになるの?」

Bundlerを経由して実行することで、MinitestのバージョンをGemfileに指定したバージョンで固定できます。また、Gemfileに書かれていないgemは読み込まれないため、もしマシンにRailsがインストールされていたとしても実行時に影響を受けません。

gemとBundlerの関係については「プロを目指す人のためのRuby入門」の12.8節で詳しく説明しているので、そちらもぜひ参照してください。

おわりに

バージョン5.11.0以降のMinitestに対応したRailsがリリースされれば、この不具合は解消されると思います。
それまではこの記事で紹介した方法で実行するようにしてみてください。

今回はRailsのアップデートによって解決しましたが、今後ももしかすると特定のgemのバージョンに起因する問題が起きるかもしれません。
「プロを目指す人のためのRuby入門」のサンプルコードで何か怪しい動きを見つけたら、技術評論社のお問い合わせページから問題を報告してください。

https://gihyo.jp/site/inquiry/book?ISBN=978-4-7741-9397-7

よろしくお願いします。