RSpecを用いたモデルの単体テスト方法

f:id:ryoutaku_jo:20190207214917p:plain

【結論】
・プログラミングにおけるテストとは、
 プログラミングが正常に動作するかを、
 プログラミングで検証する手法

・人の手ではなく、プログラミングが検証する
 メリットは下記の3つがあげられる
 ・検証する時間を短縮する
 ・仕様漏れを防ぐ
 ・リファクタリングや機能追加が容易になる

RSpec

【目次】


【本題】

テストコードを書きました

チーム開発において、ユーザーの新規登録機能を実装する際、
モデルに対してテストコードを記述する要件が設定されていた為、
今回はテストコードについてまとめることにしました。

テストの目的

そもそも、プログラミングにおけるテストとは、
プログラミングが正常に動作するかを、
プログラミングで検証する手法のことです。

それを実装する目的は、主に3点あります

・検証する時間を短縮する
 ユーザーの新規登録機能など、人の手で検証し出すと、
 一回だけなら大きな負担にならないですが、
 複数回に亘って検証を行うとなると、かなり手間が掛かります。
 テストコードを書けば、この作業を省くことが出来ます。

・仕様漏れを防ぐ
 テストコードを書く場合、どういった動作が正常なのかを
 一つづつ洗い出す必要がありますが、その過程で仕様についての
 理解が深まり、結果仕様漏れが発生することを防げます。

リファクタリングや機能追加が容易になる
 テストが一度通れば、その結果を維持さえすれば、
 正常に動作するので、リファクタリングや機能追加に注力出来ます。

RSpecとは

Rubyを元に作成されたテスト用の言語です。
Rubyと記述方法がやや異なるので、慣れるまで時間が掛かる傾向にありますが、
使いこなせることができれば、多機能で便利なツールです。

テストの種類

テストには、下記の二種類があります。
単体テスト・・・一つのプログラムが単体で正常に動作するか検証する
・統合テスト・・・複数のプログラムが連動して行われる処理が正常に行われるか検証する

今回は、ユーザーを新規登録する際の
モデルの単体テストの記述方法を紹介します。

0:モデルにバリデーションを設定する

今回はモデルの単体テストなので、
まずモデルのバリデーションを記述します。

  reg_mail_address = /\A[a-zA-Z0-9.!#$%&'*+\/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*\z/
  reg_alphanumeric_6characters = /\A[a-zA-Z0-9]{6,}+\z/
  reg_prefecture_choce = /\A(?!.*--未選択--).*\z/
  reg_only_kana = /\A[ァ-ヴ]+\z/
  reg_zip_code = /\A[0-9]{3}\-[0-9]{4}+\z/
  reg_intger_10or11_characters = /\A[0-9]{10,11}+\z/
  reg_intger_14or16_characters = /\A[0-9]{14,16}+\z/
  reg_intger_3or4_characters = /\A[0-9]{3,4}+\z/

  validates :nickname,
    presence: true,
    uniqueness: true
  validates :email,
    presence: true,
    uniqueness: true,
    format: { with: reg_mail_address }
  validates :password,
    presence: true,
    confirmation: true,
    format: { with: reg_alphanumeric_6characters }
  validates :tel_confirmation,
    presence: true,
    uniqueness: true,
    format: { with: reg_intger_10or11_characters }
  validates :first_name,
    presence: true
  validates :last_name,
    presence: true
  validates :first_name_kana,
    presence: true,
    format: { with: reg_only_kana }
  validates :last_name_kana,
    presence: true,
    format: { with: reg_only_kana }
  validates :zip,
    presence: true,
    format: { with: reg_zip_code }
  validates :prefecture,
    presence: true,
    format: { with: reg_prefecture_choce }
  validates :city,
    presence: true
  validates :block,
    presence: true
  validates :card_number,
    presence: true,
    uniqueness: true,
    format: { with: reg_intger_14or16_characters }
  validates :expiration_month,
    presence: true
  validates :expiration_year,
    presence: true
  validates :security_code,
    presence: true,
    format: { with: reg_intger_3or4_characters }

1:gemの導入する

gemの「rspec-rails」「factory_girl」を追加します。
rspec-rails・・・RSpecを利用する為のGem
※factory_girl・・・簡単にダミーのインスタンスを作成することができるGem

また web_console というgemはtest環境で動かすと
不具合が起きる可能性があるgemなのでdevelopment環境でのみ動くようにします。
新しく group :development do ~ end というブロックを作成し、その間に移動させましょう。

group :development, :test do

  gem 'rspec-rails'
  gem 'factory_girl_rails', "~> 4.4.1"
end

group :development do
  gem 'web-console', '~> 2.0'
end

Gemfileを編集したら、忘れずにbundle installをしましょう。

2:RSpecの設定をする

下記でRSpecに必要な設定ファイルを作成します。

rails g rspec:install

下記のファイルが生成されます

create  .rspec         #
create  spec                           #
create  spec/spec_helper.rb #RSpecをRails無しで利用する際の共通設定を書くファイル
create  spec/rails_helper.rb     #RailsにおいてRSpecを利用する際の共通設定を書くファイル

続いて、「.rspec」に下記の追記します。

--format documentation

これは、テストの結果をどの様に出力するか指定するコードです
下記の種類があります。

progress (もしくはp)
documentation (もしくはd)
html (もしくはh)
json (もしくはj)

3:RSpecが正常に動作するか確認する

まず、RSpecが正常に動作するか確認する為、
ターミナルで、下記を実行します。

bundle exec rspec

何もテストコードを記述していないので、
下記の様な表示が出ます。

No examples found.

Finished in 0.00031 seconds (files took 0.19956 seconds to load)
0 examples, 0 failures

4:ダミーのインスタンスを作成する

specディレクトリ直下に「factories」というディレクトリを追加し、
その中に「users.rb」という名前でファイルを作成します。

FactoryGirl.define do

  factory :user do
    nickname              "山田SAMURAI"
    email                 "yamada@gmail.com"
    password              "00000000"
    password_confirmation "00000000"
    tel_confirmation      "08011223344"
    first_name            "山田"
    last_name             "太郎"
    first_name_kana       "ヤマダ"
    last_name_kana        "タロウ"
    zip                   "123-4567"
    prefecture            "三重県"
    city                  "山々市"
    block                 "大奥町1-2"
    building              "コペンハーゲンハイツ102"
    phone_number          "0595332211"
    card_number           "4343565688779988"
    expiration_month      "1900-04-01"
    expiration_year       "2020-01-01"
    security_code         "123"
  end

end

ここでの記述内容を基に、
テストを行う為のダミーデータを生成出来ます。

5:テストコードを記述する

次にテストコードを記述します。

require 'rails_helper'

describe User do
  describe '#create' do

   it "is valid with factory girl data" do
      user = build(:user)
      expect(user).to be_valid
    end

    it "is invalid without a nickname" do
      user = build(:user, nickname: nil)
      user.valid?
      expect(user.errors[:nickname]).to include("can't be blank")
    end

    it "is invalid with a duplicate nickname" do
      create(:user)
      another_user = build(:user)
      another_user.valid?
      expect(another_user.errors[:nickname]).to include("has already been taken")
    end

    it "is invalid without a email" do
      user = build(:user, email: nil)
      user.valid?
      expect(user.errors[:email]).to include("can't be blank")
    end

    it "is invalid with a duplicate email address" do
      create(:user)
      another_user = build(:user)
      another_user.valid?
      expect(another_user.errors[:email]).to include("has already been taken")
    end

    it "is invalid with a email not includes @ " do
      user = build(:user, email: "aaaaa")
      user.valid?
      expect(user.errors[:email][0]).to include("is invalid")
    end

    it "is invalid with a email includes no character before @ " do
      user = build(:user, email: "@aaa")
      user.valid?
      expect(user.errors[:email][0]).to include("is invalid")
    end

    it "is invalid with a email includes no character after @ " do
      user = build(:user, email: "aaaa@")
      user.valid?
      expect(user.errors[:email][0]).to include("is invalid")
    end

    it "is invalid with a email includes non-alphanumeric characters " do
      user = build(:user, email: "aaあa@aaa")
      user.valid?
      expect(user.errors[:email][0]).to include("is invalid")
    end

    it "is invalid without a password" do
      user = build(:user, password: nil)
      user.valid?
      expect(user.errors[:password]).to include("can't be blank")
    end

    it "is invalid without a password_confirmation although with a password" do
      user = build(:user, password_confirmation: "")
      user.valid?
      expect(user.errors[:password_confirmation]).to include("doesn't match Password")
    end

    it "is invalid with a password that has less than 5 characters " do
      user = build(:user, password: "00000", password_confirmation: "00000")
      user.valid?
      expect(user.errors[:password][0]).to include("is too short")
    end

    it "is invalid without a tel_confirmation" do
      user = build(:user, tel_confirmation: nil)
      user.valid?
      expect(user.errors[:tel_confirmation]).to include("can't be blank")
    end

    it "is invalid without a tel_confirmation that has less than 9 characters " do
      user = build(:user, tel_confirmation: "123456789")
      user.valid?
      expect(user.errors[:tel_confirmation][0]).to include("is invalid")
    end

    it "is invalid without a tel_confirmation that has more than 12 characters " do
      user = build(:user, tel_confirmation: "123456789012")
      user.valid?
      expect(user.errors[:tel_confirmation][0]).to include("is invalid")
    end

    it "is invalid without a first_name" do
      user = build(:user, first_name: nil)
      user.valid?
      expect(user.errors[:first_name]).to include("can't be blank")
    end

    it "is invalid without a last_name" do
      user = build(:user, last_name: nil)
      user.valid?
      expect(user.errors[:last_name]).to include("can't be blank")
    end

    it "is invalid without a first_name_kana" do
      user = build(:user, first_name_kana: nil)
      user.valid?
      expect(user.errors[:first_name_kana]).to include("can't be blank")
    end

    it "is invalid without a first_name_kana includes non-KANA characters " do
      user = build(:user, first_name_kana: "カナa")
      user.valid?
      expect(user.errors[:first_name_kana][0]).to include("is invalid")
    end

    it "is invalid without a last_name_kana" do
      user = build(:user, last_name_kana: nil)
      user.valid?
      expect(user.errors[:last_name_kana]).to include("can't be blank")
    end

    it "is invalid without a last_name_kana includes non-KANA characters " do
      user = build(:user, last_name_kana: "カナa")
      user.valid?
      expect(user.errors[:last_name_kana][0]).to include("is invalid")
    end

    it "is invalid without a zip" do
      user = build(:user, zip: nil)
      user.valid?
      expect(user.errors[:zip]).to include("can't be blank")
    end

    it "is invalid with a zip includes other than integer or hyphen in the first 3 digits " do
      user = build(:user, zip: "12a-1234")
      user.valid?
      expect(user.errors[:zip][0]).to include("is invalid")
    end

    it "is invalid with a zip includes the first number is 4 digits or more " do
      user = build(:user, zip: "1234-1234")
      user.valid?
      expect(user.errors[:zip][0]).to include("is invalid")
    end

    it "is invalid with a zip includes the first number is 2 digits or less " do
      user = build(:user, zip: "12-1234")
      user.valid?
      expect(user.errors[:zip][0]).to include("is invalid")
    end

    it "is invalid with a zip includes other than integer or hyphen in the last 4 digits" do
      user = build(:user, zip: "123-123a")
      user.valid?
      expect(user.errors[:zip][0]).to include("is invalid")
    end

    it "is invalid with a zip includes the last number is 5 digits or more " do
      user = build(:user, zip: "123-12345")
      user.valid?
      expect(user.errors[:zip][0]).to include("is invalid")
    end

    it "is invalid with a zip includes the last number is 3 digits or less " do
      user = build(:user, zip: "123-123")
      user.valid?
      expect(user.errors[:zip][0]).to include("is invalid")
    end

    it "is invalid with a prefecture no chose " do
      user = build(:user, prefecture: "--未選択--")
      user.valid?
      expect(user.errors[:prefecture][0]).to include("is invalid")
    end

    it "is invalid without a city" do
      user = build(:user, city: nil)
      user.valid?
      expect(user.errors[:city]).to include("can't be blank")
    end

    it "is invalid without a block" do
      user = build(:user, block: nil)
      user.valid?
      expect(user.errors[:block]).to include("can't be blank")
    end

    it "is invalid without a card_number" do
      user = build(:user, card_number: nil)
      user.valid?
      expect(user.errors[:card_number]).to include("can't be blank")
    end

    it "is invalid with a card_number that has less than 13 characters " do
      user = build(:user, card_number: "1234567890123")
      user.valid?
      expect(user.errors[:card_number][0]).to include("is invalid")
    end

    it "is invalid with a card_number that has more than 17 characters " do
      user = build(:user, card_number: "12345678901234567")
      user.valid?
      expect(user.errors[:card_number][0]).to include("is invalid")
    end

    it "is invalid with a card_number includes non-integer characters" do
      user = build(:user, card_number: "12345678901234ab")
      user.valid?
      expect(user.errors[:card_number][0]).to include("is invalid")
    end

    it "is invalid with a duplicate card_number" do
      create(:user)
      another_user = build(:user)
      another_user.valid?
      expect(another_user.errors[:card_number]).to include("has already been taken")
    end

    it "is invalid without a expiration_month" do
      user = build(:user, expiration_month: nil)
      user.valid?
      expect(user.errors[:expiration_month]).to include("can't be blank")
    end

    it "is invalid without a expiration_year" do
      user = build(:user, expiration_year: nil)
      user.valid?
      expect(user.errors[:expiration_year]).to include("can't be blank")
    end

    it "is invalid without a security_code" do
      user = build(:user, security_code: nil)
      user.valid?
      expect(user.errors[:security_code]).to include("can't be blank")
    end

    it "is invalid with a security_code that has less than 2 characters " do
      user = build(:user, security_code: "12")
      user.valid?
      expect(user.errors[:security_code][0]).to include("is invalid")
    end

    it "is invalid with a security_code that has more than 5 characters " do
      user = build(:user, security_code: "12345")
      user.valid?
      expect(user.errors[:security_code][0]).to include("is invalid")
    end

    it "is invalid with a security_code includes non-integer characters" do
      user = build(:user, security_code: "12a")
      user.valid?
      expect(user.errors[:security_code][0]).to include("is invalid")
    end

  end
end

6:テストを実行する

下記をターミナルで打ち込み、テストを実行します。

bundle exec rspec

7:エラーを検証する

エラーが発生した場合は、エラー文を確認し、
適宜「binding.pry」を噛ませて、
値が正常に取得できるか確認します。

総括

正直、テストコードは今まで全く分かりませんでしたが、
ここに来て理解が深まると共に、
テストの有用性を改めて知りました。
今後は、出来れば最初にテストを書ける様になりたいですね。

《今日の学習進捗》

チーム開発:13日目
ユーザー新規登録機能のバリデーションと
テストコードの記述が完了。

学習開始からの期間 :62日
今日までの合計時間:637h
今日までに到達すべき目標時間:566h
目標との解離:71h
「10,000時間」まで、

残り・・・「9363時間!」