Rspecでテストコード を書く① 〜テストの準備とUserモデル編〜

TECH::CAMP卒業まであと26日になりました。

1ヶ月を切ると焦りを覚えてきますが、

やるべき優先順位を自分なりに考えながら勉強していこうと思う今日この頃です。

 

TECH::CAMPのカリキュラム内では、「???」しか浮かばなかったテストコード ですが、

テストは大事!と言われているので、今回は個人アプリでテストコード を練習した記録を書いていこうと思います。

 

 

 

テスト用のgemをダウンロード

テストコード の記載に使う下記のgemをダウンロードしました。

(Gemfileのgroup: test do ~ endの中に記入します)

gem 'rspec-rails'
gem 'factory_bot_rails'

 

また、これは後からダウンロードしてみたのですが

gem 'faker', "~> 2.8"

一部のモデルにしかまだ使用していません。(理解不足なので練習中)

 

Rspecを使う準備

gem 'rspec'は、gemをインストールするだけでは使えません。

まずは専用のディレクトリを作ります。

コマンドで

$ rails g spec:install

と打ち込むと、ターミナルに下記のように表示されます。

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

4つファイル(ディレクトリも含む)作ったでーと言われました。

 

.rspec(Gemfileの3つぐらい上にある)に下記を追記します。

--format documentation

 

 FactoryBotを使う準備

※FactoryBotを使うと、rspecで使う擬似的なユーザーや投稿などを、FactoryBotのファイルにまとめて記載しrspecで使うことができます。

ここで作られるユーザーや投稿はあくまでテスト用のものであるため、

sequelProなどで保存を確認することはできません。

(確認するときはbinding.pryで動作を止めて確認します)

たくさんテストを書くときには、同じようなコードをたくさん書かずに済むため重宝するgemです。

 

spec/rails_helper.rb

RSpec.configure do |config|
config.include FactoryBot::Syntax::Methods ←これを追加
~省略〜
end

上記を記載すると、

user = FactoryBot.build(:user)

 と記載していたのを

user = build(:user)

と省略記法で書けるようになります。

たくさんテストを書くことを想定すると、大文字小文字が混ざった記載を省けるのはありがたい!

 

ではFactoryBotで擬似的なユーザー、プロジェクト、コードを作成します。

 factories/user.rb

FactoryBot.define do

factory :user do
password = Faker::Internet.password(min_length:8)
username {Faker::Internet.username(15)}
email {Faker::Internet.email}
password {password}
password_confirmation {password}
end

end

とりあえずuserのみFakerを使用しています。

まず、passwordを最初に定義しているのは、ユーザー登録時に2回同じものを登録する必要があるからです。

usernameは15文字を超えるとバリデーションが作動して、テスト中にエラーになることがあるため、15文字指定にしています。(max_length: 15にするとエラーが出たんです・・・undefined methodだって。なんでや)

 

Fakerを使うと何がいいかというと、ランダムな文字の組み合わせで必要な情報を作成してくれることです。

テスト用の名前とかメールアドレスって、単純なもの(aaa@gmail.comとか)を作りがちですが、Fakerを使うと毎回違うダミーを作成してくれるので、厳密なテストが可能になります。

 

テストコード を書いてみる

ようやく本題のrspecのコードを書いていきます。

specディレクトリ配下にmodelsディレクトリを作成し、

その中にuser_spec.rbファイルを作成します。

そしてまずは

require 'rails_helper'
describe Project do
end

のみを記載し、テストを実行してみます。

テストの実行コマンドは

$ bundle exec rspec

だけでも良いですが、たくさんファイルが増えてきた場合は

bundle exec rspec spec/models/user_spec.rb

という風にファイルを指定してやると良いです。

上記のコードだけだと、条件を指定していないので、

1 example, 0 failures

とでて、無条件にテストを通過します。

 

ではテストの条件を書いていきます。

require 'rails_helper'
describe Project do
context "userを保存できない場合" do
it "usernameがないと登録できないこと" do
user = build(:user, username:nil)
user.valid?
expect(user.errors[:username]).to include("can't be blank")
end
 
it "usernameが16文字以上だと登録できないこと" do
user = build(:user, username: "abcdefghijklmnop")
user.valid?
expect(user.errors[:username]).to include("is too long (maximum is 15 characters)")
end
 
it "emailが他ユーザーと重複していると登録できないこと" do
user = create(:user, email: "aaa@email.com")
another_user = build(:user, email: "aaa@email.com")
another_user.valid?
expect(another_user.errors[:email]).to include("has already been taken"
)
end
 
it "emailがないと登録できないこと" do
user = build(:user, email:nil)
user.valid?
expect(user.errors[:email]).to include("can't be blank", "is invalid")
end
 
it "emailに「-_.」以外の記号があると登録できないこと" do
user = build(:user, email:"aaa:@aaa.com")
user.valid?
expect(user.errors[:email]).to include("is invalid")
end
 
it "passwordがないと登録できないこと" do
user = build(:user, password:nil)
user.valid?
expect(user.errors[:password]).to include("can't be blank")
end
 
it "passwordが8文字以下であれば登録できないこと" do
user = build(:user, password: "abcdefg")
user.valid?
expect(user.errors[:password]).to include("is too short (minimum is 8 characters)")
end
 
it "passwordとpassword_confirmationが異なると登録できないこと" do
user = build(:user, password_confirmation: "00000001")
user.valid?
expect(user.errors[:password_confirmation]).to include("doesn't match Password")
end
end
 
context "userを保存できる場合" do
it "usernameが15文字以内であれば登録できること" do
expect(build(:user, username: "abcdefghijklmno")).to be_valid
end
 
it "passwordが8文字以上であれば登録できること" do
expect(build
(:user, password: "00000000", password_confirmation: "00000000"))
.to be_valid
end
 
it "username, email, passwordが存在すれば登録できること" do
expect(build(:user)).to be_valid
end
 
it "emailに「-_.」の記号が入っていても登録できること" do
expect(build(:user, email: "a-a_a.a@aaa.com")).to be_valid
end
end
end

 

 

userを保存するときの様々な条件を想定して書いていきます。

先にバリデーションをかけた内容について書くとわかりやすいかなと思います。

 

簡単に解説を。

it "usernameがないと登録できないこと" do
user = build(:user, username:nil)
user.valid?
expect(user.errors[:username]).to include("can't be blank")
end

例えばこれだと、2行目で保存したいユーザーを定義しています。

ユーザーはFactoryBotで定義したものを使うのですが、

「build(:user)」 に追加で「username: nil」を入れることで

FactoryBotで作ったusernameをなしにすることができます。

ターミナルでいうと、下記のようなコマンドを打ったことになります。

 

user = User.new(

username: nil,

email: "Fakerで作ったデタラメアドレス",

password: "Fakerで作ったデタラメパスワード",

password_confirmation: "passwordに入れたものと同じ文字列")

 

上記をいちいち書かなくてよくて、欲しい条件だけ上書きすればいいのですから、

FactoryBotとFaker超便利!ってことですね〜

 

そして3行目の「valid?」は「マッチャ」と呼ばれる条件を示しています。

valid?は「バリデーションされれば」

4行目のincludeは「含んでいれば」

と読み替えることができます。

3行目のexpect(~~~).to は、望まれる内容(こう返ってくるだろうなという予想)を記入します。

 

実際にターミナルで、先ほど入力したuser=User.new(~~)を打ち込み、

確認してみます。(rails c でコンソールにしてから実行します)

user = User.new(

username: nil,

email: "email@email.com",

password: "12345678",

password_confirmation: "12345678")

 

下記のようにuserが定義されます。

=> #<User:0x00007f900ced95b0

id: nil,

username: nil,

email: "email@email.com",

password_digest: "$2a$12$KilRDXdXFXnfqM.9POfILeHaf4S.eiIkepb8GQqphOoSs55RN9zBi",

created_at: nil,

updated_at: nil>

 

そこで下記のコマンドを打つと

user.valid?

ターミナルに下記のように表示されます。

User Exists (15.5ms)  SELECT  1 AS one FROM `users` WHERE `users`.`email` = 'email@email.com' LIMIT 1

=> false

「false」 と出ました。

バリデーションがかかっていると、falseになります。

バリデーション にかからないuserを定義するとtrueになります。

 

次に下記コマンドを打つと、バリデーション にかかった際のエラー文を教えてくれます。

user.errors

下記のように出ました。

=> #<ActiveModel::Errors:0x00007f900d13bb28

@base=

  #<User:0x00007f900ced95b0

   id: nil,

   username: nil,

   email: "email@email.com",

   password_digest: "$2a$12$KilRDXdXFXnfqM.9POfILeHaf4S.eiIkepb8GQqphOoSs55RN9zBi",

   created_at: nil,

   updated_at: nil>,

@details={:username=>[{:error=>:blank}]},

@messages={:username=>["can't be blank"]}>

 

赤字の部分が大事!

usernameに対してエラー文"can't be blank"が出ていますよ、という意味です。

この一連の流れをテストで仮定して実行し、確認しているわけです。

 

上記の流れを当てはめて、他の条件についても書いていきます。

 

 

「登録できる場合」に関しては、

コードの記載は簡単です。

it "usernameが15文字以内であれば登録できること" do
expect(build(:user, username: "abcdefghijklmno")).to be_valid
end

エラー文が出ないような条件を与えてやり、 be_validマッチャ(バリデーションをクリアする)を書いてやります。

 

 

まとめ 

正直最初にテストコード をみたときは、

テストコード ってなんか意味あるの?ぐらいの考えでした。

ですが自分でアプリを作っているうちに、

いろいろな条件でバリデーションを考えるようになり、

バリデーションの動作を確認できるテストコード の意味がようやくわかってきました。

 

 

特にFakerでランダムに文字列を与えてくれるのはとてもありがたい機能だと思います。

ただしここで問題が。

emailには特定の記号しか含んではいけないというバリデーションをかけています。

Fakerの方には「この記号を含んではいけない」という規制をかけていないため、

たまにuserを生成できないメールアドレスを作ってくれちゃう時があります。

なので何回かテストしてると、通ったり通らなかったり。

Faker に規制をかける方法があるらしいので、後日調べて実装しようと思います。

 

また、もう一つ課題が。

重複したemailが登録できないことを確認するテストで、

ターミナルで確認したエラー文でテストを実行すると通らないという事態です。

テスト実行時には、ターミナルには「エラー文は空白のはずやで」と出ます。

なので一旦空白にしているのですが、これは解決しないとなあと思っていますので、

解決できたらまた更新したいと思っています。

→解決できました!

最初にcreateで作ったユーザーがのemailが、既にローカルのデータベースにあるemailと重複してしまっていたので、そもそも最初にダミーで入れるはずだったuserがcreateできてなかったみたい? です。

メールアドレスを別のものにしたらテスト通りました。