Railsのテストの仕方（Minitest編）

# Minitest
MinitestはRails標準のテスティングフレームワークです。特に追加でgemを入れる必要もなく、使うことができます。
Minitestを使うことでルーティング、各コントローラ間のワークフロー、データベースとのやり取り、メソッドの挙動、ブラウザに返されるHTMLファイルの内容、等々、様々なものを簡単にテストすることができます。
本稿ではMinitestの使い方や、それを取り巻く周辺知識について解説していきます。  
  
## **各種設定・構成**

### テストデータベースと設定

デフォルトではRailsアプリは3つのデータベース（test、development、production）を持ちます。
テスト・開発・本番環境でそれぞれ専用のデータベースを用いることで、その他の環境を汚すことなく安全に運用することができます。  
各々の環境におけるデータベース設定は、`config/database.yml`に記載されています。
例えばデータベースがSQLiteの場合、デフォルトでは以下のような設定になっています。  

```Ruby
default: &default
  adapter: sqlite3
  pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
  timeout: 5000

development:
  <<: *default
  database: db/development.sqlite3

test:
  <<: *default
  database: db/test.sqlite3

production:
  <<: *default
  database: db/production.sqlite3
```

|設定項目|意味|
|:-----|:-----|
|adapter:|データベースの種類を指定します。|
|pool:|接続プール数を指定します。|
|timeout:|タイムアウト時間を指定します。|
|database:|データベースファイルまでのパスを指定しています。|

ところで`$ bundle exec rails dbconsole`コマンドを使うと、
`database:`で指定されたデータベースにアクセスすることができます。  
実際のデータベースに接続することで、設定内容を確認したり、SQL文を発行してテーブルやレコードの新規作成をすることもできます。
ただし、テーブルの作成等を行う場合は、`$ rails(rake) db`コマンドやマイグレーションを行った方がミスを少なくできるでしょう。  
  
### テスト実行前の準備

未完了のマイグレーションファイルが存在する場合、テスト実行前にdevelopmentデータベースに対してマイグレーション（`$ bundle exec rails db:migrate`）を実行する必要があります。
これはテスト用のデータベースのスキーマを最新にする必要があるためです。  

### フォルダ構成
`rails new`コマンドを実行すると、デフォルトでテスト用のディレクトリ/ファイルが生成されます。  
以下はそのディレクトリ構成と役割です。  

|ディレクトリ構成|役割|
|:---------|:---------|
|test|||
|├ controllers|コントローラ・ビュー・ルーティングを纏めたテストを置く|
|├ fixtures|テストデータ生成用のファイルを置く|
|├ helpers|ビューヘルパーのテストを置く|
|├ integration|コントローラ同士のやり取りのテストを置く|
|├ mailers|メーラーのテストを置く|
|├ models|モデル用のテストを置く|
|├ system|システムテスト（Javascriptとか）を置く|
|application_system_test_case.rb|システムテストのデフォルト設定を書く|
|test_helper.rb|テストのデフォルト設定を書く|


## **記法**
基本的には、ActiveSuportがあればテストは書けます。  

```Ruby
require 'test_helper'
 
class ArticleTest < ActiveSupport::TestCase
  test "the truth" do
    assert true
  end
end
```

上記のコードを整理しましょう。  
`require 'test_helper'`によって、test/test_helper.rbに記載されているデフォルト設定が読み込まれます。すべてのテストファイルにこのコードが記載されますので、このファイルに追加したメソッドはテスト全体で利用することができます。
独自のアサーションを定義したい場合はtest_helper.rbに書くと手っ取り早くテストで利用できます。  
  
次に、
  
> `class ArticleTest < ActiveSupport::TestCase`  

こちらのクラスはActiveSupport::TestCaseを継承しています。ActiveSupport::TestCaseのスーパークラスはRuby標準のテスティングフレームワークであるMinitest::Testです。
そのため、Minitest::TestのアサーションはすべてArticleTestで利用できます。
  
ところでMinitest::Testの場合、  

```Ruby
class SampleTest < Minitest::Test
  def test_sample
    ...
  end
end
```

このように、メソッド名にtest_とついたものをテスト実行時にランダムに実行していきますが、
Railsの場合、もっと読みやすいテスト名にすることができます。  
  
```Ruby
test "the truth" do
  assert true
end
```

testメソッドは引数に文字列とブロックを受け付けます。
`test "the truth"`と書くことで、行頭にtestを追加し、スペースをアンダーバーに変換してくれます（この場合はdef test_the_truthと一緒）。
testメソッドを使うことで、テストの命名に気を使わなくても済みます。実際のテストコードはブロックに記述します。  
  
### setup/teardownメソッド

テスト実行前に毎回実行してほしい処理があれば、setupメソッドが使えます。  

```Ruby
class ArticlesControllerTest < ActionDispatch::IntegrationTest
  # 各テストの実行前に呼ばれる
  setup do
    @article = articles(:one)
  end 
  ...
```

Ruby標準のMinitestでは`def setup`と書いていましたが、Railsの場合、上記のようにわかりやすい記法が使えます。
  
setupメソッドとは反対に、テスト実行後に行いたい処理があれば、teardownメソッドを使います。

```Ruby
class ArticlesControllerTest < ActionDispatch::IntegrationTest
  # 各テストの実行後に呼ばれる
  teardown do
    # コントローラがキャッシュを使っている場合、テスト後にリセットしておくとよい
    Rails.cache.clear
  end 
  ...
```

こちらも省略記法が使えます。  

### アサーション
Ruby標準のMinitestでは数多くのアサーションが利用できます。以下に代表例を記載します。  

|アサーション|目的|
|:---------------------|:------------------------|
|assert(test, [msg])    |式testの評価値は真であると主張します。  |
|assert_not(test, [msg])|式testの評価値は偽であると主張します。 |
|assert_equal(expected, actual, [msg])|expected == actualの評価値は真であると主張します。|
|assert_not_equal(expected, actual, [msg])|expected == actualの評価値は偽であると主張します。|
|assert_raises(exception1, exception2, ...) { block }|渡されたブロックから、引数に渡された例外のいずれかが発生すると主張します。|
|assert_operator(obj1, :演算子, [obj2], [msg])|obj1.演算子(obj2)の評価値は真であると主張します。なお、assert_operatorの第二引数に渡す演算子はシンボルで表記することにご注意ください。|
|flunk([msg])|必ず失敗すると主張します。これはテストが未完成であることを示すのに便利です。|

assertメソッドの[msg]の箇所に文字列を渡すことで、テスト失敗時のエラーメッセージを指定することもできます。  
なお、こうしたテスティングフレームワークはモジュール化されており、Railsはそこに固有のアサーションを追加しています。以下に代表例を記載します。

|アサーション|目的|
|:---------------------|:------------------------|
|assert_nothing_raised { block }|渡されたブロックで例外が発生しないことを確認します。|
|assert_difference(expressions, difference = 1, message = nil, &block)|第一引数に渡した式の戻り値が、ブロックを実行することで、第二引数に渡した値だけ変化することを確認します。なお、expressionsは文字列であることにご注意ください。|
|assert_no_difference(expressions, message = nil, &block)|第一引数に渡した式の戻り値が、ブロックを実行した後も変わらないことを確認します。なお、expressionsは文字列であることにご注意ください。|
  

### 結合テスト

あるコントローラの様々なアクションをテストしたり、複数のコントローラ同士のやり取りをテストすることを結合テストと呼びます。
Railsではアプリケーションの一連のワークフローをテストするための専用のクラスが用意されています。  

```Ruby
require 'test_helper'

class UsersControllerTest < ActionDispatch::IntegrationTest
  test "should get index" do
    get users_index_url
    assert_response :success
  end

end
```

上記は`$ bundle exec rails g controller [コントローラ名] [アクション名]...`のコマンドによって、
`test/controllers`配下に生成されたテストファイルです。  
内容を一つ一つ整理していきましょう。  
  
`ActionDispatch::IntegrationTest`は結合テスト（integration test）用のクラスです。
`ActiveSupport::TestCase`を継承していますので、これまで紹介してきた数多くのテストヘルパーが使えます。  
また、このクラス専用のヘルパーもあります。
以下に代表例を記載致します。  

|メソッド|目的|
|:------------|:------------|
|HTTP動詞 path, params: { key: value }|指定したパスにHTTPリクエストを飛ばし、@responseを返します。第一引数には名前付きルートを渡すか、"/users"のようにパスを表す文字列を渡します。第二引数にparams:オプションを渡すことで、リクエストのパラメータを指定することもできます。|
|assert_response(type, message = nil)|レスポンスが特定のステータスコードを持っていることを主張します。:successを指定するとステータスコード200-299を指定したことになり、同様に:redirectは300-399、:missingは404、:errorは500-599にそれぞれマッチします。|
|assert_redirected_to path|リクエストの結果、pathにリダイレクトされることを検証します。|
|assert_recognizes({controller: 'コントローラ名', action: 'アクション名', id: 'id値', view: 'フォルダ名/ファイル名' }, {path: 'ルート', method: :HTTP動詞, view: 'フォルダ名/ファイル名' })|第二引数で指定したHTTPリクエスト＆URLの組み合わせで、第一引数のコントローラ＆アクションにルーティングされるかを検証します。routes.rbに該当パスが複数あった場合でも使えます。出力されるViewファイルも検証できます。|
|assert_routing({ method: 'HTTP動詞', path: 'ルート' }, controller: 'コントローラ名', action: 'アクション名'…)|コントローラ＆アクションの組み合わせ等からパスが生成できること、またパスから指定のコントローラ＆アクションにアクセスできることを検証します。複数マッチするルートがあっても使えます。第一引数にブロックを使う場合、引数全体を()で囲まなければ引数ではなくブロックと解釈されてしまうのでご注意ください。|
|assert_select(要素, セレクタ, [条件], [メッセージ])|選択された要素が条件に一致することを検証します。assert_selectをネストする場合、親で指定された要素をコレクションし、それに対して子がassert_selectします。例えばある順序つきリストに2つずつリストが含まれている場合、合計4つをテストすることも、2つずつテストすることもできます。|

以下のコマンドによって、結合テスト用のファイルを新規作成することもできます。ファイルは`test/integration`ディレクトリ配下に生成されます。  

```Ruby
$ bundle exec rails generate integration_test [テストファイル名]
  invoke  test_unit
  create    test/integration/テストファイル名.rb
```

## **テストデータの生成・管理**

テストを行うにあたって、テスト用のデータをデータベースに保存できると便利です。
Railsではfixtureという仕組みによって、テストデータの定義とカスタマイズができます。  

### fixture

テスト用のサンプルデータのことです。`$ bundle exec rails g model`コマンドでモデルを生成する際に、`test/fixtures`配下に.yml拡張子のfixtureファイルが生成されます。fixtureファイルに記載されたデータはテスト実行前にテスト用のデータベースに導入(保存)することができます。
なお、Railsはデフォルトで、`test/fixtures`配下のフィクスチャファイルを読み込み、モデルやコントローラのテストで利用します。  

fixtureの特徴としては以下になります。  

- モデルやコントローラのテスト実行時に、対応するテーブルのデータをすべて削除してから、fixtureのデータをテーブルに読み込みます。
- データはYAML形式で記述されます。
  - fixture定義名と、インデントされたカラム名： 値 の組み合わせのリストになります。
  - レコード間は空白で区切ります。
  - 行頭に# を置くとコメント行になります。
- ActiveRecordのインスタンスを生成します。
- ERB形式でRubyコードを埋め込めます。

以下にサンプルデータを示します。  

```Ruby
# test/fixtures/hoges.ymlの内容
one:
  name: Hoge san
  email: hoge@hoge.com

# テスト中にレコードを取り出す方法
@one = hoges(:one)
@one.name #=> "Hoge san"
```

なお、fixtureに限った話ではありませんが、Minitestを実行中にテスト用のデータベースに保存されたレコードは、idカラムの値がランダムな数字になることにご注意ください。  
  
fixtureを使って簡単にアソシエーションを表現することもできます。
例えば1:N関係モデルの場合、以下のように書きます。  

```Ruby
class Hoge < ApplicationRecord
  has_many: fuges
end

class Fuge < ApplicationRecord
  belongs_to: hoge
end

# test/fixtures/hoges.ymlの内容
one:
  name: aaaa

# test/fixtures/fuges.ymlの内容
two:
  email: fuge@fuge.com
  hoge: one	      # アソシエーションで定義したモデル名: fixturesの定義名（ラベル）
```

もちろん、idを直接指定する書き方もできます。  

```Ruby
# test/fixtures/hoges.ymlの内容
one:
  id: 1
  name: aaaa

# test/fixtures/fuges.ymlの内容
two:
  email: fuge@fuge.com
  hoge_id: 1
```

ですが、こうした親キー/外部キーを指定する書き方は可読性が下がり、データを柔軟に変更することも難しくなる可能性があります。
ActiveRecordは、渡されたfixtureの定義名（ラベル）から任意のfixtureのidを知ることができますので、
ラベルを参照する記法の方が便利かもしれません。

一方、fixtureを使うやり方ではありませんが、
テスト中に関連付けを設定しているレコードを作成する方法として、以下のような記法も使えます。  

```Ruby
setup do
  @hoge = Hoge.create
  @fuge = @hoge.fuges.create(email: "fuge@fuge.com")
end
```

Minitestではテスト中にレコードを新規作成する場合、デフォルトではidカラムの値がランダムな数字になります。
そのため、アソシエーションを表現する場合は、モデルのidを明示的に指定しなければなりません。
しかし上記の記法の場合は、idを省略することができます。

これまではテスト用のデータベースにデータを保存する方法について解説してきました。
しかし、そもそもデータを保存（永続化）する必要がない場合は、
`モデルオブジェクト.new`
を使ってシンプルにモデルのインスタンスを生成してテストしましょう。  

```Ruby
test "name should be correct" do
  @user = User.new(name: "Tanaka Satoshi")
  assert @user.valid?
end
```

### モデルテスト用のジェネレーター

新たにモデルテスト用のファイルやfixtureを生成したい場合は、以下のジェネレーターが使えます。  

```Ruby
$ bundle exec rails g test_unit:model [model_name] [column_name:data_type]
create  test/models/[model_name]_test.rb
create  test/fixtures/[model_name].yml
```
  
### テストの実行

rails(またはrake)コマンドによって、テストを実行することができます。
なお、Rails5以降はテスト実行時に、どのテストを実行するのか、についてオプションがつけられるようになりました。  

```Ruby
# (test/ 以下の)すべてのテストを実行する
$ bin/rails test

# 特定フォルダ以下のすべてのテストを実行する
$ bin/rails test test/models

# 複数のフォルダ以下のすべてのテストを実行する
$ bin/rails test test/models test/jobs

# 特定のファイルの特定の行のテストを実行する
$ bin/rails test test/models/user_test.rb:14
```

## **出典**
[Minitest でテスト、Rails のテスト (その1)](https://qiita.com/5t111111/items/5bb86896f85ac9cfc43f)
[Rails テスティングガイド](https://railsguides.jp/testing.html)
[ActiveRecord :: FixtureSet < Object](https://edgeapi.rubyonrails.org/classes/ActiveRecord/FixtureSet.html)
[Railsドキュメント/設定ファイル(config)](http://railsdoc.com/config)

## **環境**

macOS High Sierra バージョン10.13.6
Rails 5.2.1
ruby 2.5.1p57 (2018-03-29 revision 63029) [x86_64-darwin17]