紙一重の積み重ね

35歳のエンジニアがなれる最高の自分を目指して、学んだことをこつこつ情報発信するブログです。

【RSpec】Everyday Rails学習ログ①~RSpecのインストールからモデルスペックまで~

f:id:yokoyantech:20190817194150p:plain

はじめに

8/17に参加した Saitama.rb #20で実施した、RSpecのもくもく学習ログです。

なぜRSpecを学ぶのか

私が関わっているRailsプロダクトの品質を安定させ、継続的に開発を進めていくためです。

現在私が携わっているRailsプロダクトは5.2系で動いています。ゆくゆくはRails6に上げたいのですが、残念ながらテストコードがありません。そもそも、会社にテストコードを書くという文化がありません。正確には、私含めテストコードがきちんと書ける人がいません。手動でテストを繰り返す・・・というのは、非常に手間暇がかかる上に、デグレしていないという品質の担保ができないため、早くここから脱却したいと考えています。

何より、自分のプロダクトの品質が安定すれば、仕事のトラブルも減り、家族と過ごす時間も増えるはずです。

チームのリーダーとして、「みんなテストコード書こうぜ!」と言う前に、まず自分ができるようにならないと・・・、ということで、評判が良い Everyday Rails を購入して、改めてRSpecを学ぶことにしました。(以前、ドットインストールでRSpecを学ぼうと試みたことがあるのですが、その時は挫折しました。)

leanpub.com

Rails & RSpec環境構築

前半は環境構築です。

Cloud9の作成

手持ちのWindows端末だとbundle installするのに手間なので、今回はAWS Cloud9でUbuntu18を使用しました。

コードの取得

以下URLから git clone します。

github.com

 

RSpecのインストール

  • Gemfileに追加。
 group :development, :test do
   gem 'rspec-rails', '~> 3.6.0'
   # 略
 end
  • インストール
$ bundle install

# 以下略
Fetching rails 5.1.1
Installing rails 5.1.1
Fetching rspec-support 3.6.0
Installing rspec-support 3.6.0
Fetching rspec-core 3.6.0
Installing rspec-core 3.6.0
Fetching rspec-expectations 3.6.0
Installing rspec-expectations 3.6.0
Fetching rspec-mocks 3.6.0
Installing rspec-mocks 3.6.0
Fetching rspec-rails 3.6.1
Installing rspec-rails 3.6.1

# 以下略

テストDBの作成

$ bin/rails db:create:all
Created database 'db/development.sqlite3'
Created database 'db/test.sqlite3'
Created database 'db/production.sqlite3'

RSpec設定ファイルの生成

$ bin/rails generate rspec:install
Running via Spring preloader in process 14794
      create  .rspec
      create  spec
      create  spec/spec_helper.rb
      create  spec/rails_helper.rb

RSpec設定

.rspec は隠しファイル。

$ ls -la
total 92
drwxrwxr-x 14 ubuntu ubuntu 4096 Aug 17 04:49 .
drwxr-xr-x  4 ubuntu ubuntu 4096 Aug 17 04:37 ..
drwxrwxr-x  8 ubuntu ubuntu 4096 Aug 17 04:38 .git
-rw-rw-r--  1 ubuntu ubuntu  599 Aug 17 04:38 .gitignore
-rw-rw-r--  1 ubuntu ubuntu   22 Aug 17 04:49 .rspec
  • .rspec にformatを追記。
--require spec_helper
--format documentation

binstubのインストール

group :development do
  # 略
  gem 'spring-commands-rspec'
end
  • インストールする。
$ bundle install

# 略

Fetching spring-commands-rspec 1.0.4
Installing spring-commands-rspec 1.0.4

# 略
  • binstubの生成
$ bundle exec spring binstub rspec
* bin/rspec: generated with spring

はじめての実行

  • テストコードがまだないので空振り
$ bin/rspec
Running via Spring preloader in process 15277
No examples found.

Finished in 0.00043 seconds (files took 0.11799 seconds to load)

ジェネレータ設定

  • generate コマンドで不要なファイルが生成されるようにする。
  • application.rb に追加。
module Projects
  class Application < Rails::Application
    # Initialize configuration defaults for originally generated Rails version.
    config.load_defaults 5.1

    # Settings in config/environments/* take precedence over those specified here.
    # Application configuration should go into files in config/initializers
    # -- all .rb files in that directory are automatically loaded.

    config.generators do |g|
      g.test_framework :rspec,
      fixtures: false,
      view_specs: false,
      helper_specs: false,
      routing_specs: false
    end
  end
end

モデルスペックの作成

はじめてRSpecでテストコードを書きました!

ブランチ切り替え

$ git checkout -b my-03-models origin/02-setup

ブランチを切り替えた後は、bundle install が必要だった。

UserモデルのSpec作成

$ bin/rails g rspec:model user
Running via Spring preloader in process 16129
      create  spec/models/user_spec.rb

ひとまず実行

$ bin/rspec
Running via Spring preloader in process 16252
/home/ubuntu/.rvm/gems/ruby-2.6.3/gems/activerecord-5.1.1/lib/active_record/connection_adapters/abstract_adapter.rb:81: warning: deprecated Object#=~ is called on Integer; it always returns nil
/home/ubuntu/.rvm/gems/ruby-2.6.3/gems/activerecord-5.1.1/lib/active_record/connection_adapters/abstract_adapter.rb:81: warning: deprecated Object#=~ is called on Integer; it always returns nil

User
  add some examples to (or delete) /home/ubuntu/environment/everydayrails-rspec-2017/spec/models/user_spec.rb (PENDING: Not yet implemented)

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

  1) User add some examples to (or delete) /home/ubuntu/environment/everydayrails-rspec-2017/spec/models/user_spec.rb
     # Not yet implemented
     # ./spec/models/user_spec.rb:4


Finished in 0.00209 seconds (files took 2.88 seconds to load)
1 example, 0 failures, 1 pending

テストコードの仮実装 & 実行

  • 自動生成されたスペックに追加
require 'rails_helper'

RSpec.describe User, type: :model do
  # 性、名、メール、パスワードがあれば有効な状態であること
  it "is valid with a first name, last name, email, and password"

  # 名がなければ無効な状態であること
  it "is invalid without a first name"

  # 性がなければ無効な状態であること
  it "is invalid without a last name"

  # メールアドレスがなければ無効な状態であること
  it "is invalid without an email address"

  # 重複したメールアドレスなら無効な状態であること
  it "is invalid with a duplicate email address"

  # ユーザーのフルネームを文字列として返すこと
  it "returns a user's full name as a string"

end

- 現在の状態で実行 - it以下の記載した英文毎に結果が出力されて見やすい!! - まだ中身が無いので、pendingになっている

$ bin/rspec
Running via Spring preloader in process 16614
/home/ubuntu/.rvm/gems/ruby-2.6.3/gems/activerecord-5.1.1/lib/active_record/connection_adapters/abstract_adapter.rb:81: warning: deprecated Object#=~ is called on Integer; it always returns nil

User
  is valid with a first name, last name, email, and password (PENDING: Not yet implemented)
  is invalid without a first name (PENDING: Not yet implemented)
  is invalid without a last name (PENDING: Not yet implemented)
  is invalid without an email address (PENDING: Not yet implemented)
  is invalid with a duplicate email address (PENDING: Not yet implemented)
  returns a user's full name as a string (PENDING: Not yet implemented)

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

  1) User is valid with a first name, last name, email, and password
     # Not yet implemented
     # ./spec/models/user_spec.rb:6

  2) User is invalid without a first name
     # Not yet implemented
     # ./spec/models/user_spec.rb:9

  3) User is invalid without a last name
     # Not yet implemented
     # ./spec/models/user_spec.rb:12

  4) User is invalid without an email address
     # Not yet implemented
     # ./spec/models/user_spec.rb:15

  5) User is invalid with a duplicate email address
     # Not yet implemented
     # ./spec/models/user_spec.rb:18

  6) User returns a user's full name as a string
     # Not yet implemented
     # ./spec/models/user_spec.rb:21


Finished in 0.00485 seconds (files took 0.4084 seconds to load)
6 examples, 0 failures, 6 pending

Specの記載

  • [私は、~が、~になることを期待する]
    • I expect something to be something else
  • [私は、~が、~にならないことを期待する]

    • I expect something not_to be something else
  • be_balidはRSpecが提供するマッチャ(matcher)

    • 期待値と実際の値を比較して、結果を返すオブジェクト
# 性、名、メール、パスワードがあれば有効な状態であること
it "is valid with a first name, last name, email, and password" do
  user = User.new(
    first_name: "Aaron",
    last_name: "sumner",
    email: "tester@example.com",
    password: "dottle-nouveau-pavilion-tights-furze",
  )
  expect(user).to be_valid
end
  • 実行
$ bin/rspec
Running via Spring preloader in process 17004
/home/ubuntu/.rvm/gems/ruby-2.6.3/gems/activerecord-5.1.1/lib/active_record/connection_adapters/abstract_adapter.rb:81: warning: deprecated Object#=~ is called on Integer; it always returns nil

User
  is valid with a first name, last name, email, and password
  is invalid without a first name (PENDING: Not yet implemented)
  is invalid without a last name (PENDING: Not yet implemented)
  is invalid without an email address (PENDING: Not yet implemented)
  is invalid with a duplicate email address (PENDING: Not yet implemented)
  returns a user's full name as a string (PENDING: Not yet implemented)

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

  1) User is invalid without a first name
     # Not yet implemented
     # ./spec/models/user_spec.rb:17

  2) User is invalid without a last name
     # Not yet implemented
     # ./spec/models/user_spec.rb:20

  3) User is invalid without an email address
     # Not yet implemented
     # ./spec/models/user_spec.rb:23

  4) User is invalid with a duplicate email address
     # Not yet implemented
     # ./spec/models/user_spec.rb:26

  5) User returns a user's full name as a string
     # Not yet implemented
     # ./spec/models/user_spec.rb:29


Finished in 0.03489 seconds (files took 0.34002 seconds to load)
6 examples, 0 failures, 5 pending

バリデーションのテスト

  • 2つ目のテストに追加
# 名がなければ無効な状態であること
it "is invalid without a first name" do
  user = User.new(first_name: nil)
  user.valid?
  # first_name属性にエラーメッセージが付いていることを期待
  expect(user.errors[:first_name]).to include("can't be blank")
end

バリデーションのテストが失敗するか検証

RSpecを変える

  • to_not に変える
# 名がなければ無効な状態であること
it "is invalid without a first name" do
  user = User.new(first_name: nil)
  user.valid?
  # first_name属性にエラーメッセージがついていることを期待
  expect(user.errors[:first_name]).to_not include("can't be blank")
end
  • 実行結果
    • 期待通りエラーになる
$ bin/rspec
Running via Spring preloader in process 18543
/home/ubuntu/.rvm/gems/ruby-2.6.3/gems/activerecord-5.1.1/lib/active_record/connection_adapters/abstract_adapter.rb:81: warning: deprecated Object#=~ is called on Integer; it always returns nil

User
  is valid with a first name, last name, email, and password
  is invalid without a first name (FAILED - 1)
  is invalid without a last name (PENDING: Not yet implemented)
  is invalid without an email address (PENDING: Not yet implemented)
  is invalid with a duplicate email address (PENDING: Not yet implemented)
  returns a user's full name as a string (PENDING: Not yet implemented)

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

  1) User is invalid without a last name
     # Not yet implemented
     # ./spec/models/user_spec.rb:25

  2) User is invalid without an email address
     # Not yet implemented
     # ./spec/models/user_spec.rb:28

  3) User is invalid with a duplicate email address
     # Not yet implemented
     # ./spec/models/user_spec.rb:31

  4) User returns a user's full name as a string
     # Not yet implemented
     # ./spec/models/user_spec.rb:34


Failures:

  1) User is invalid without a first name
     Failure/Error: expect(user.errors[:first_name]).to_not include("can't be blank")
       expected ["can't be blank"] not to include "can't be blank"
     # ./spec/models/user_spec.rb:21:in `block (2 levels) in <top (required)>'
     # /home/ubuntu/.rvm/gems/ruby-2.6.3/gems/spring-commands-rspec-1.0.4/lib/spring/commands/rspec.rb:18:in `call'
     # -e:1:in `<main>'

Finished in 0.04358 seconds (files took 0.32631 seconds to load)
6 examples, 1 failure, 4 pending

Failed examples:

rspec ./spec/models/user_spec.rb:17 # User is invalid without a first name

モデルを変える

  • モデルからバリデーションを消す
  # validates :first_name, presence: true
  • 実行
    • 期待通りエラーになる
      • Specでは名前がないユーザは無効になることを期待しているが、アプリ側がその期待する振る舞いをしていないため。
$ bin/rspec
Running via Spring preloader in process 18659

User
  is valid with a first name, last name, email, and password
  is invalid without a first name (FAILED - 1)
  is invalid without a last name (PENDING: Not yet implemented)
  is invalid without an email address (PENDING: Not yet implemented)
  is invalid with a duplicate email address (PENDING: Not yet implemented)
  returns a user's full name as a string (PENDING: Not yet implemented)

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

  1) User is invalid without a last name
     # Not yet implemented
     # ./spec/models/user_spec.rb:25

  2) User is invalid without an email address
     # Not yet implemented
     # ./spec/models/user_spec.rb:28

  3) User is invalid with a duplicate email address
     # Not yet implemented
     # ./spec/models/user_spec.rb:31

  4) User returns a user's full name as a string
     # Not yet implemented
     # ./spec/models/user_spec.rb:34


Failures:

  1) User is invalid without a first name
     Failure/Error: expect(user.errors[:first_name]).to include("can't be blank")
       expected [] to include "can't be blank"
     # ./spec/models/user_spec.rb:21:in `block (2 levels) in <top (required)>'
     # /home/ubuntu/.rvm/gems/ruby-2.6.3/gems/spring-commands-rspec-1.0.4/lib/spring/commands/rspec.rb:18:in `call'
     # -e:1:in `<main>'

Finished in 0.04634 seconds (files took 0.33149 seconds to load)
6 examples, 1 failure, 4 pending

Failed examples:

rspec ./spec/models/user_spec.rb:17 # User is invalid without a first name

少し複雑なスペック

  • DB保存後、同一ユーザのインスタンスを作成
# 重複したメールアドレスなら無効な状態であること
it "is invalid with a duplicate email address" do
  User.create(
    first_name: "Joe",
    last_name: "Tester",
    email: "tester@example.com",
    password: "bottle-nouveau-pavilion-tights-furze",
  )
  user = User.new(
    first_name: "Joe",
    last_name: "Tester",
    email: "tester@example.com",
    password: "bottle-nouveau-pavilion-tights-furze",
  )
  user.valid?
  expect(user.errors[:email]).to include("has already been taken")
end
  • 実行結果
$ bin/rspec
Running via Spring preloader in process 19961

User
  is valid with a first name, last name, email, and password
  is invalid without a first name
  is invalid without a last name
  is invalid without an email address (PENDING: Not yet implemented)
  is invalid with a duplicate email address/home/ubuntu/.rvm/gems/ruby-2.6.3/gems/concurrent-ruby-1.0.5/lib/concurrent/concern/logging.rb:20: warning: instance variable @logger not initialized

  returns a user's full name as a string (PENDING: Not yet implemented)

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

  1) User is invalid without an email address
     # Not yet implemented
     # ./spec/models/user_spec.rb:32

  2) User returns a user's full name as a string
     # Not yet implemented
     # ./spec/models/user_spec.rb:53


Finished in 0.13759 seconds (files took 0.33328 seconds to load)
6 examples, 0 failures, 2 pending

ProjectモデルのSpecファイル作成

$ bin/rails g rspec:model project
Running via Spring preloader in process 20069
      create  spec/models/project_spec.rb

Projectモデルのスペック実装

require 'rails_helper'

RSpec.describe Project, type: :model do
  # ユーザー単位では重複したプロジェクト名を許可しないこと
  it "does not allow duplicate project names per user" do
    user = User.create(
      first_name: "Joe",
      last_name: "Tester",
      email: "joetester@example.com",
      password: "dottle-nouvea-pavilion-tights-furze",
    )

    user.projects.create(
      name: "Test Project",
    )

    new_project = user.projects.build(
      name: "Test Project",
    )

    new_project.valid?
    expect(new_project.errors[:name]).to include("has already been taken")
  end

  # 二人のユーザが同じ名前を使うことをは許可すること
  it "allows two users to share a project name" do
    user = User.create(
      first_name: "Joe",
      last_name: "Tester",
      email: "joetester@example.com",
      password: "dottle-nouvea-pavilion-tights-furze",
    )

    user.projects.create(
      name: "Test Project",
    )

    other_user = User.create(
      first_name: "Jane",
      last_name: "Tester",
      email: "janetester@example.com",
      password: "dottle-nouvea-pavilion-tights-furze",

    )

    other_project = other_user.projects.build(
      name: "Test Project",  
    )

    expect(other_project).to be_valid
  end
end

実行結果

$ bin/rspec
ubuntu:~/environment/everydayrails-rspec-2017 (my-03-models) $ bin/rspec
Running via Spring preloader in process 20604

Project
  does not allow duplicate project names per user
  allows two users to share a project name

User
  is valid with a first name, last name, email, and password
  is invalid without a first name
  is invalid without a last name
  is invalid without an email address (PENDING: Not yet implemented)
  is invalid with a duplicate email address
  returns a user's full name as a string (PENDING: Not yet implemented)

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

  1) User is invalid without an email address
     # Not yet implemented
     # ./spec/models/user_spec.rb:32

  2) User returns a user's full name as a string
     # Not yet implemented
     # ./spec/models/user_spec.rb:53


Finished in 0.29442 seconds (files took 0.35177 seconds to load)
8 examples, 0 failures, 2 pending

まとめ

ということで、今回はRSpecの環境構築から、Userモデルのテスト、Projectモデルのテストを行いました。EXCEL仕様書を使って手動で動作確認するのではなく、テストコードでテストが繰り返し実行できるのは、素晴らしいです!!引き続き学習を進めていきます!!