RailsにRspecをいれてモデルのテストを書いてみよう[初心者向け]

こんにちは。
Cryptogamesで働いているじゅん([@BoNingennnN](https://twitter.com/BoNingennnN))です。

Railsでは標準でminitestが入ってきます。
しかし、弊社ではminitestを使わずRSpecを使っています。
また、個人プロジェクトでもRSpecを使っています。

今回はRailsプロジェクトにRSpecを導入する方法を説明し、そしてテストの手始めとしてモデルのテストを書いていこうと思います！

## まずはRailsプロジェクトにRSpecを導入しよう
既存のプロジェクトにRspecを導入する際には、既存のtestを削除する必要があります。
まずtestディレクトリを削除しましょう。

続いてRspecのgemをいれていきます

``` :Gemfile
group :development, :test do
  gem 'rspec-rails'
end
```

```
bundle install
```

もしまだ既存のプロジェクトでtest用のdbを作成していない場合はそれを作成します。
config/database.ymlにtest用の記述があることを確認したら以下のコマンドを打ちましょう

```
rails db:create
```

これでtest用のdbの作成が完了です。
続いてRspecの導入を進めます。
以下のコマンドでRspecの設定ファイルなどを生成しましょう。

``` 
rails g rspec:install
```

ここはお好みになりますが、testの実行結果が見やすいように以下の記述を.rspec内に書いておくといいです。

``` :.rspec
 --format documentation
```

これでRspecの導入は完了しました！
早速以下のコマンドを実行してテストを実行しましょう。

```
bundle exec rspec
```

結果は、、

```
No examples found.
Finished in 0.00079 seconds (files took 0.25041 seconds to load)
0 examples, 0 failures
```

大成功です。
まだテストを書いていないので、0 expamleです。
exampleはテストの数、failuresは通らなかったテストの数を返します。
まだテストを書いていないので、もちろんexample、failures共に0です。

では続いて超初歩的なモデルのテストを書いていきましょう。


## モデルのテストをRspecで書こう
ここからはモデルのテストを書きます。
ここではすでにPostモデルがあると仮定します。
(ここは各自のアプリケーションにあるモデルと置き換えて考えてください🙇‍♂️)

``` ruby:post.rb
class Post < ApplicationRecord
  validates :title, presence: true, length: {maximum: 20}
  validates :content, presence: true

  def content_length
    self.content.length
  end
end
```
こんな単純なモデルはありえないですが、入門ということで簡単なモデルにしました。

既存のモデルのテストを作るには以下のコマンドを実行します。

```
rails g rspec:model post
```
すると以下のファイルが生成されます。

``` ruby:spec/models/post_spec.rb
require 'rails_helper'

RSpec.describe Post, type: :model do
  pending "add some examples to (or delete) #{__FILE__}"
end
```
ここで先ほどと同様にtestを実行してみましょう。

```
bundle exec rspec
```

すると先ほどとは異なる出力が見られます。

```
Pending: (Failures listed here are expected and do not affect your suite's status)

  1) Post add some examples to (or delete) /Users/junya/workspace/myprojects/rails-rspec-test/spec/models/post_spec.rb
     # Not yet implemented
     # ./spec/models/post_spec.rb:4


Finished in 0.00684 seconds (files took 4.16 seconds to load)
1 example, 0 failures, 1 pending
```
注目して欲しいのは、exmapleが0から1に変わった部分です。
これは先ほど生成したpost_spceを生成したからです。
pendingとなっているのは、「このテストは一旦後回しにしますよー」という意味です。
生成されるspecファイルは、初期でpendingとなっています。
この部分を実際に書き換えていきましょう！

と、その前にまずモデルテストでテストすべきことを書いていきます。
基本的には、そのモデルのバリデーションのテストを書きます。
さらに、そのモデルがインスタンスメソッドを持っているのであればそのテストも書きますし、クラスメソッドを持っていればクラスメソッドのテストも書きます。また、scopeに関するテストももちろん必要です。

今回の例だと、バリデーションとインスタンスメソッドがあるので、それぞれのテストを書いていきます。

### バリデーションのテストを書いてみる
早速バリデーションのテストを書いていきましょう。

```ruby:post_spec.rb
require 'rails_helper'

RSpec.describe Post, type: :model do
  it "is valid with title and content" do
    post = Post.new(title: "test", content: "testcontent")
    expect(post).to be_valid
  end
end
```
先ほどpendingと書かれていた部分を削除し、実際にテストを書いてみました。
テストを書く際には

```
it "テストの内容の説明" do
  テストの処理
end
```
という形で書きます。
モデルテストを書く際には、まずちゃんとバリデーションが通ること(be_valid)をテストします。
ここでテストを実際に実行してみましょう。

```
bundle exec rspec
```

出力は以下のようになります。

```
.

Finished in 0.01384 seconds (files took 3.45 seconds to load)
1 example, 0 failures
```
見事に1exampleが実行され、failure(テストの失敗)は0です！
ここで一度テストをわざと失敗させてみましょう。

先ほど作成したテストの中身を少し書き換えてみます。

```ruby:post_spec.rb
require 'rails_helper'

RSpec.describe Post, type: :model do
  it "is valid with title and content" do
    post = Post.new(title: "test", content: "testcontent")
    expect(post).not_to be_valid #toからnot_toに変更
  end
end
```
そしてテストを実行してみると、、、

```
F

Failures:

  1) Post is valid with title and content
     Failure/Error: expect(post).not_to be_valid
       expected #<Post id: nil, title: "test", content: "testcontent", created_at: nil, updated_at: nil> not to be valid
     # ./spec/models/post_spec.rb:6:in `block (2 levels) in <top (required)>'

Finished in 0.0629 seconds (files took 3.57 seconds to load)
1 example, 1 failure

Failed examples:

rspec ./spec/models/post_spec.rb:4 # Post is valid with title and content
```

見事に1example 1failureになっています！
テストが失敗したことがわかるかと思います。
こんな感じで、もし通らないテストがあるとちゃんと表示されます。

先ほど変更した部分を元に戻したら、バリデーションテストの続きを書いていきましょう。

今回のPostモデルには3つのバリデーションがあります。
・titleが存在すること
・titleの最大文字数は20文字
・contentが存在すること

それらのバリデーションのテストを実際に書いていきます

``` ruby:post_spec.rb
  it "is invalid without title" do
    post = Post.new(title: nil)
    post.valid?
    expect(post.errors[:title]).to include("can't be blank")
  end

  it "is invalid withoug content" do
    post = Post.new(content: nil)
    post.valid?
    expect(post.errors[:content]).to include("can't be blank")
  end

```
titleとcontentのpresence: trueのバリデーションのテストをまず書きました。
もしtitleがnilなら「titleは空欄じゃダメですよ」っていうエラーがあるよね、ということをテストしています。(contentも同様)
このパターンはバリデーションのテストの際によく出てくるパターンです。

続いて文字数の部分のテストを書きます。

```ruby:post_spec.rb
  it "is invalid when title is more than 20 characters" do
    post = Post.new(title: ("a" * 21))
    post.valid?
    expect(post.errors[:title]).to include("is too long (maximum is 20 characters)")
  end
```
21文字だとエラーが出るよね、ということをテストしています。
本来だったら20文字の場合はエラーだなくてvalidだよねというのもテストした方が良さそうですが、ずいぶん長くなってきたので割愛します。

以上がバリデーションのテストです。
バリデーションのテストは結構パターンがあるので慣れればすんなり書けます。
また、バリデーションをうっかり忘れていた、なんてことも気付けたりしますので、そういう意味でもバリデーションテストはしっかり書いておいたほうがいいです。

### インスタンスメソッドのテストを書いてみる
続いてインスタンスメソッドのテストも書いてみます。
今回の例だと、

``` ruby:post.rb
  def content_length
    self.content.length
  end
```

このcontent_lengthというインスタンスメソッドのテストを書きます。
インスタンスメソッドやクラスメソッドでは、適当に入力する値をいれ、その出力が正しいかをテストします。
と言ってもよく分からないかもしれないので、実際にコードを書いていきましょう。

``` ruby:post_rspec.rb
  it "returns length of content" do
    post = Post.create(title: "test", content: ("a" * 100))
    expect(post.content_length).to eq(100)
  end
```

なんとなく見ればわかるかと思いますが、contentが100文字なpostを作成し、
post.content_lengthが100と等しいことをテストしています。
これは非常に簡単な例ですが、複雑になってきても基本はこれです。
こんな感じでどんどんインスタンスメソッドやクラスメソッドのテストも書いていきましょう！！

## さいごに
本当はもっと複雑なメソッドのテスト等も書きたかったのですが、最初から難しいめのテストを書くと挫折してしまう可能性があると思ったので、今回は超簡単なものだけにしました。
(本当はただ書くのが面倒くさかっただけかもです。笑)

今回の例は初歩の初歩で、factory_botも使っていませんし、モデル以外のテストも全く出てきませんでした。
実際のプロジェクトでテストを書くとなると、学ぶべきことは他にも色々ありますが、とりあえずはこのモデルの基本的なテストを書けるようになっておけばあとは色々ググりながらどうにかなるかと思います！

もし今担当しているプロジェクトや、個人開発のプロジェクトでテストを書いていないのであれば、まずはモデルテストから書いてみてください！