見出し画像

【Rails勉強ログ#3】ユーザーモデルの単体テストコードを書く rspec | factorybot | faker | gimei |

前回バリデーションを設定したので、今回はテストコードの作成。
自分のやったことを最短距離でまとめつつ、理解を深めるために解説を加える。


やりたいこと

  1. ユーザー新規登録画面で登録できる文字種などを指定するバリデーションを書いたが、問題なく機能しているか確認したい。

  2. テストするたびに毎回、「ニックネームやメールアドレス、パスワード、名前など」を手打ちしてテストするのは面倒だし、打ち間違いするとテスト結果がおかしくなるから自動化したい。

  3. 毎回同じテストを行なって合格したことを客観的に担保したい。

方針

  1. 自動でテストしてくれる機能の集合体である「rspec」をインストールして利用する。

  2. 会員登録テストをするための個人情報を作ってくれる「FactoryBot」をインストールして使用する。

  3. 毎回ランダムな名前やメールアドレスを生成してくれる「Faker」をインストールして使用する。

  4. ランダムな氏名を漢字やカタカナで生成してくれる「Gimei」をインストールして使用する。

簡単な解説

RSpec

rspecは、「試してほしいこと」をプログラムしておくと一発で全部試して結果を教えてくれるプログラムの集合体。 「こうすればうまくいくはず」「こうだったらうまくいかないはず」というパターンを列挙して、コマンド一撃で何十種類ものテストをやってくれる。すごい。

FactoryBot

読んで字の如く、ロボット工場。
例えば「名前はJohn Smithにして、メールアドレスはxxxx@mail.comにして」みたいに指示しておくと、毎回同じ個人情報を作ってくれる。ありがたい。

Faker

フェイクする人。つまり偽物製造マシン。
ランダムなニックネームやメールアドレス、氏名を生成してくれる。
毎回新しい名前で登録してくれる。ただし英語しか生成できないので、「西園寺秀幸」とかは生成してくれない。
パスワードもランダムで作ってくれるが、ランダムすぎるせいで稀に全部数字のパスワードとかを作ってしまい事故る。かわいい。

Gimei

偽名をつくるツール。
日本語の名前であることからもわかる通り、日本語の偽名を作ってくれる。
これで「西園寺秀幸」も「高幡五郎左衛門」も作れるが、ランダムなので一生出てこないかも。
たとえば「名前は漢字でないと受け付けない」といったバリデーションがあるサイトでは必要になる。便利。

導入手順

Gemを追加する

Gemfile

group :development, :test do
  # See <https://guides.rubyonrails.org/debugging_rails_applications.html#debugging-with-the-debug-gem>
  gem "debug", platforms: %i[ mri mingw x64_mingw ]
  gem 'rspec-rails', '~> 4.0.0'
  gem 'factory_bot_rails'
  gem 'faker'
  gem 'gimei'
end

ここで、Gemfileの末尾ではなく、開発環境とテスト環境でのみ動作する範囲にGemを定義しておく。
本番環境にはできるだけ要らんものは入れないという心がけ。

terminal ※アプリケーションのディレクトリで

bundle install

rspecをインストール・初期設定する

bundle installしただけでは、まだ使えない。
terminal

rails g rspec:install

インストールが完了すると、以下のログが流れる。

create  .rspec
create  spec
create  spec/spec_helper.rb
create  spec/rails_helper.rb

rspecは、そのままでは「裏でコソコソテストする系職人」なので、
ちゃんとテスト結果を提出するよう指示を出しておく必要がある。
生成されている .rspec というファイルに、以下を記述する。

--require spec_helper
--format documentation

1行目はデフォルトで書かれているので、2行目を追加すればOK。

「やってほしいテスト」を書くファイルを用意する

terminal

rails g rspec:model user

今回はユーザーログインに関係するバリデーションをテストしてほしいので、userという名前のモデルに対応するファイルを生成いただいた。
ファイルを見てみると以下のように書かれている。
spec/models/user_spec.rb

require 'rails_helper'

RSpec.describe User, type: :model do
  pending "add some examples to (or delete) #{__FILE__}"
end

1行目のrequire 'rails_helper' で、必要なファイルを読み込むことで動作している模様。
これによって、テストに用いる基本的な設定やメソッドを読み込んで、毎回ゼロから設定しなくても良いようにしてくれるっぽい。

ここまでで、「やってほしいテストを書くファイル」と、「テストをやってくれる人」を入手した状況。

FactoryBotを導入する

Gemfileへの記述とbundle install はさっき済ませたので、ロボット工場を用意していく。

  1. spec/factories のフォルダを作成する

  2. spec/factories/users.rb のファイルを作成する

※factoryBot導入後にrails g rspec:model userした場合は、上記のフォルダ・ファイルが自動で出来ているかもしれない。

Faker, Gimeiを導入する

さっきGemfileへの記述と、bundle installしたので、もう使える。

実際にできたFactoryBotがこちら

spec/factories/users.rb

require 'gimei'

FactoryBot.define do
  factory :user do
    nickname           { Faker::Name.initials(number: 2) }
    email              { Faker::Internet.email }
    password           { '1a' + Faker::Internet.password(min_length: 6) }
    password_confirmation { password }
    first_name         { Gimei.name.first.kanji }
    last_name          { Gimei.name.last.kanji }
    first_name_kana    { Gimei.name.first.katakana }
    last_name_kana     { Gimei.name.last.katakana }
    birthday           { Faker::Date.birthday(min_age: 18, max_age: 65) }
  end
end

上から順番に、

  • ニックネームは、アルファベット2文字でランダムに生成

  • メールアドレスは、ランダムに生成

  • パスワードは、ランダムすぎて「全部数字」などの事故を防ぐために、「1a」のあとにランダムな文字列を生成するよう設定

  • パスワード(確認)は、パスワードと同じものをセットするよう設定

  • 偽名は、漢字のところとカタカナのところをそれぞれ設定

  • 誕生日は、間違って2歳とか1200歳のユーザーが爆誕しないよう、年齢縛りを設定

というコードになっている。

実際にできたテストコードがこちら

※全部貼るのはちょっと気が引けるので一部抜粋。ほんとうはもっと長い。
spec/models/user_spec.rb

require 'rails_helper'

RSpec.describe User, type: :model do
  before do
    @user = FactoryBot.build(:user)
  end

  describe 'ユーザー新規登録' do
    context 'ユーザー登録ができる時' do
      it '全ての値が正しく入力されていれば登録できる' do
        expect(@user).to be_valid
      end
    end

    context 'ユーザー登録ができない時' do
      it 'nicknameが空だと登録できない' do
        @user.nickname = ''
        @user.valid?
        expect(@user.errors.full_messages).to include("Nickname can't be blank")
      end

      it 'emailが空だと登録できない' do
        @user.email = ''
        @user.valid?
        expect(@user.errors.full_messages).to include("Email can't be blank")
      end

      it 'emailに@が含まれていないと登録できない' do
        @user.email = 'testexample.com'
        @user.valid?
        expect(@user.errors.full_messages).to include('Email is invalid')
      end

      it '重複したemailが存在する場合登録できない' do
        @user.save
        another_user = FactoryBot.build(:user, email: @user.email)
        another_user.valid?
        expect(another_user.errors.full_messages).to include('Email has already been taken')
      end

      it 'passwordが空だと登録できない' do
        @user.password = ''
        @user.valid?
        expect(@user.errors.full_messages).to include("Password can't be blank")
      end

      it 'passwordが5文字以下だと登録できない' do
        @user.password = 'a1a1a'
        @user.password_confirmation = 'a1a1a'
        @user.valid?
        expect(@user.errors.full_messages).to include('Password is too short (minimum is 6 characters)')
      end
      
      〜〜〜〜中略〜〜〜〜
      
    end
  end
end

解説

RSpec.describe User, type: :model do の記述で、これからUserのモデルに対してテストを行うことを明示。

before do
    @user = FactoryBot.build(:user)
  end

beforeで「テストの前に」、FactoryBotにユーザーインスタンスを作ってもらう。
そして、作ってもらったインスタンスを@user というインスタンス変数にパッケージングする。
イメージとしては、「プロフィールが一式詰まった箱」が@user という短い表記になった形。
その箱には「名前」とか「メールアドレス」が詰まっていて、自由に取り出したり、上書きしたりできる。

describe 'ユーザー新規登録' do
    context 'ユーザー登録ができる時' do
      it '全ての値が正しく入力されていれば登録できる' do

ここでは、
describe で「ユーザー新規登録機能のテストするよ」
context(文脈)で、「正しくできる場合のテストするよ」
it で、「正しく情報を入力できたら会員登録できるかどうかテストするよ」
という宣言。
describe > context > it で、だんだん範囲が狭くなっている。
「機能全体」>「機能がうまくいく場合」>「うまくいくはずの操作」 という絞り込み。

正常系テスト

正常系テストとは、「開発者が想定する正しい入力がされたとき、正しい挙動が返される」ことをテストするもの。
自販機で言えば、「お金を入れて、金額が十分足りているときにボタンを押せば飲み物が出る」ことをテストするイメージ。

      it '全ての値が正しく入力されていれば登録できる' do
        expect(@user).to be_valid
      end

expect(@user)で、さっきパッケージングされたプロフィール一式に対して、「期待することはね・・・」と話し始めたところ。

エクスペクト・パトローナム!!!のexpectである。
あれは、「ハリーは守護霊を期待しています!!」という呪文だったというワケ。

わいの雑念

で、そのあとの「.to be_valid」 は、「それがバリデーション要件を満たしていること」という意味。

つまり、前半と組み合わせると、「@userのプロフィールがバリデーションのルールに沿っていることを期待している」という意味合いになる。
この1行のコードの結果、「PATH(合格)」または「FAIL(不合格)」の結果が得られる。

異常系テスト

異常系テストは、正常系テストの逆で「開発者が想定していない動きをユーザーがしたときに、ちゃんと動かないかどうか」をテストするもの。

動かないかどうかをテストする、とは?

と思うかもしれないが、わかりやすく言えば
「自販機にお金を入れずにボタンを押すと、飲み物が出る」ではマズイのである。
つまり、「自販機にお金が入れられていないとき、ボタンを押しても飲み物が出ない」ことを確認しておかねばならない。

      it 'nicknameが空だと登録できない' do
        @user.nickname = ''
        @user.valid?
        expect(@user.errors.full_messages).to include("Nickname can't be blank")
      end

これが異常系コードの一部。
@user.nickname = ' '
この部分で、「@userプロフィールの、ニックネームのところを空白に書き換えちゃうよ!」という操作を行なっている。
@user.valid?
この部分では、「ニックネーム空白にしたけど、よかったかな?」と尋ねている。良いワケがない。
expect(@user.errors.full_messages).to include("Nickname can't be blank")
この部分は長いので分割して解説していく。
expect
エクスペクト・パト(略
@user.errors.full_messages
この文言は、ユーザーインスタンスに対するエラーメッセージの全文を取得するコード。
インスタンスってのはプロフィール入りの箱だと思ってもらってOK。
.to include("Nickname can't be blank")
.to_includeで「〇〇を含んでいること」という意味になる。
expectと組み合わせて、「〇〇を含んでいることを期待する」という文になる。
("Nickname can't be blank")
これは、出てきてほしいエラーメッセージの文章。Railsのdeviseでユーザ管理機能を実装していたら、デフォルトではこんなメッセージが出てくる。
つまり、
expect(@user.errors.full_messages).to include("Nickname can't be blank")
この文章の意味は、
「@userに対して出されたエラーメッセージが"Nickname can't be blank"を含んでいることを期待するよ!」つまり、期待通りのエラーが出たら合格!というテストなのである。

自販機の例に戻ると、「お金を入れずに商品のボタンを押したとき、ジュースが出なければ合格!」というテストにあたる。

ちなみに異常系テストはめっちゃ数が多い。
ニックネームが空だとエラー、名前が空だとエラー、アレはだめ、コレもだめ、と…あらゆるNGパターンを「本当にNGになっているか」テストしないといけない。
クドイようだが自販機の例にもどると、「お金を入れずにお茶のボタンを押すと何も出ないが、レッドブルのボタンを押すとレッドブルが出ちゃう」という自販機ではダメなのである。
全部のエラーパターンを網羅しておく必要があるということ。

まとめ

今回はテストを簡単にしてくれる四銃士、rspec, factorybot, faker, gimeiを使ってテストコードを書いたのでまとめた。

いくつかのページから断片的に情報を集めて実装したので、この際ひとつのページに全部まとまってりゃ(自分が)便利だと思って、まとめてみた次第。

とても長いコードで、初見のときは絶望したものだが、書いてみると案外わかりやすくて楽しい。
毎回ハリーが頭をよぎるからかもしれない。もしくは初見時はディメンターがいたのかもしれない。

今回のコードは「単体テストコード」であって、各モデル(今回はユーザー)ごとに設定したバリデーションがきちんと機能しているかどうかを調べるものだった。
今後アプリ全体が出来上がってくると「結合テストコード」なる、画面上の機能を勝手にポチポチ押してテストしてくれる機能もあって、そっちはもっと楽しい。いまから実装が楽しみ。

備忘録おしまい。

この記事が気に入ったらサポートをしてみませんか?