はじめに
8/17に参加した Saitama.rb #20で実施した、RSpecのもくもく学習ログです。
なぜRSpecを学ぶのか
私が関わっているRailsプロダクトの品質を安定させ、継続的に開発を進めていくためです。
現在私が携わっているRailsプロダクトは5.2系で動いています。ゆくゆくはRails6に上げたいのですが、残念ながらテストコードがありません。そもそも、会社にテストコードを書くという文化がありません。正確には、私含めテストコードがきちんと書ける人がいません。手動でテストを繰り返す・・・というのは、非常に手間暇がかかる上に、デグレしていないという品質の担保ができないため、早くここから脱却したいと考えています。
何より、自分のプロダクトの品質が安定すれば、仕事のトラブルも減り、家族と過ごす時間も増えるはずです。
チームのリーダーとして、「みんなテストコード書こうぜ!」と言う前に、まず自分ができるようにならないと・・・、ということで、評判が良い Everyday Rails を購入して、改めてRSpecを学ぶことにしました。(以前、ドットインストールでRSpecを学ぼうと試みたことがあるのですが、その時は挫折しました。)
Rails & RSpec環境構築
前半は環境構築です。
Cloud9の作成
手持ちのWindows端末だとbundle install
するのに手間なので、今回はAWS Cloud9でUbuntu18を使用しました。
コードの取得
以下URLから git clone
します。
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仕様書を使って手動で動作確認するのではなく、テストコードでテストが繰り返し実行できるのは、素晴らしいです!!引き続き学習を進めていきます!!