関連記事
- 勉強会BDD<1>RSpecとCapybaraースタート
- 勉強会BDD<2>RSpecとCapybaraーモデル
- 勉強会BDD<3>RSpecとCapybaraーユーザー認証
- 勉強会BDD<4>RSpecとCapybaraーポイントシステム
ポイントシステム(1) -- 数値の変化をテストする
仕様のおさらい
- ユーザーがログインに成功すると、「ログインポイント」としてユーザーに1ポイントが与えられる。
- ただし、「ログインポイント」はユーザーごとに1日1回しか与えられない。日の区切りは日本時間午前5時とする。
- また、土曜日にログインすると、通常の「ログインポイント」の他に「土曜ログインボーナス」として2ポイントが与えられる。
数値の変化に関するテストの書き方
- 仕様:ユーザーがログインに成功すると、ユーザーの保有ポイントが1増える。
- Customer#points
- specの修正
# spec/models/customer.rbの.authenticateの中
example 'ログインに成功すると、ユーザーの保有ポイントが1増える' do
expect {
Customer.authenticate(customer.username, 'correct_password')
}.to change { customer.points }.by(1)
end
# , + n
- 説明
expect { X }.to change { Y }.by(Z)
# X:数値変化の原因になるコード
# Y:変化を調べたい数値を返すメソッド
# Z:数値の変化量
- テスト
1 || Run options: include {:locations=>{"./spec/models/customer_spec.rb"=>[107]}mple 'ログインに成功すると、ユーザーの保有ポイントが1増える'
2 ||
3 || Customer.authenticate
4 || ログインに成功すると、ユーザーの保有ポイントが1増える (FAILED - 1)
5 ||
6 || Failures:
7 ||
8 spec/models/customer_spec.rb|110 error| Failure/Error: }.to change { customer.points }.by(1) NoMethodError: undefined method `points' for #<Customer:0x007f
9 || # ./spec/models/customer_spec.rb:108:in `block (2 levels) in <top (required)>'
10 ||
11 || Finished in 0.15944 seconds (files took 2.81 seconds to load)
12 || 1 example, 1 failure
13 ||
14 || Failed examples:
15 ||
16 || rspec ./spec/models/customer_spec.rb:107 # Customer.authenticate ログインに成功すると、ユーザーの保有ポイントが1増える
メソッドの仮実装
- specの修正
example 'ログインに成功すると、ユーザーの保有ポイントが1増える' do
allow(customer).to receive(:points).and_return(0)
expect {
Customer.authenticate(customer.username, 'correct_password')
}.to change { customer.points }.by(1)
end
- テスト
1 || Run options: include {:locations=>{"./spec/models/customer_spec.rb"=>[83]}}
2 ||
3 || Customer.authenticate
4 || ユーザー名とパスワードに該当するCustomerオブジェクトを返す
5 || パスワードが一致しない場合nilを返す
6 || 該当するユーザー名が存在しない場合はnilを返す
7 || パスワード未設定のユーザーを拒絶する
8 || ログインに成功すると、ユーザーの保有ポイントが1増える (FAILED - 1)
9 ||
10 || Failures:
11 ||
12 spec/models/customer_spec.rb|110 error| Failure/Error: expect { expected result to have changed by 1, but was changed by 0
13 ||
14 || Finished in 0.7688 seconds (files took 2.7 seconds to load)
15 || 5 examples, 1 failure
16 ||
17 || Failed examples:
18 ||
19 || rspec ./spec/models/customer_spec.rb:107 # Customer.authenticate ログインに成功すると、ユーザーの保有ポイントが1増える
- ペンディング
example 'ログインに成功すると、ユーザーの保有ポイントが1増える' do
pending 'Customer#pointsが未実装'
allow(customer).to receive(:points).and_return(0)
expect {
Customer.authenticate(customer.username, 'correct_password')
}.to change { customer.points }.by(1)
end
ポイントシステム(2) -- ログインポイントの付与
Rewardモデル
- modelの生成
% bin/rails g model reward
- migrationファイルの修正及び適用
# db/migrate/20150924070150_create_rewards.rb
class CreateRewards < ActiveRecord::Migration
def change
create_table :rewards do |t|
t.references :customer
t.integer :points
t.timestamps null: false
end
end
end
# run migration
bin/rake db:migrate
bin/rake db:test:prepare
- modelの修正
# app/models/customer.rb
class Customer < ActiveRecord::Base
has_many :rewards
... snip ...
# app/models/reward.rb
class Reward < ActiveRecord::Base
belongs_to :customer
end
- 不要なspecの削除
% rm spec/models/reward_spec.rb
Customer#pointsメソッドのテストと実装
- specの修正
# spec/models/customer_spec.rb
describe Customer, '#points' do
let(:customer) { create(:customer, username: 'taro') }
example '関連付けられたRewardのpointsを合計して返す' do
customer.rewards.create(points: 1)
customer.rewards.create(points: 5)
customer.rewards.create(points: -2)
expect(customer.points).to eq(4)
end
end
- テスト
1 || Run options: include {:locations=>{"./spec/models/customer_spec.rb"=>[116]}}
2 ||
3 || Customer#points
4 || 関連付けられたRewardのpointsを合計して返す (FAILED - 1)
5 ||
6 || Failures:
7 ||
8 spec/models/customer_spec.rb|124 error| Failure/Error: expect(customer.points).to eq(4) NoMethodError: undefined method `points' for #<Customer:0x007f9a113
9 ||
10 || Finished in 0.08741 seconds (files took 2.9 seconds to load)
11 || 1 example, 1 failure
12 ||
13 || Failed examples:
14 ||
15 || rspec ./spec/models/customer_spec.rb:119 # Customer#points 関連付けられたRewardのpointsを合計して返す
16 ||
- modelの修正
class Customer < ActiveRecord::Base
... snip ...
def points
rewards.sum(:points)
end
def self.authenticate(username, password)
... snip ...
- テスト
[devnote@hooni:~/documents/study/oiax] % bin/rspec spec/models/customer_spec.rb:116
Run options: include {:locations=>{"./spec/models/customer_spec.rb"=>[116]}}
Customer#points
関連付けられたRewardのpointsを合計して返す
Finished in 0.12071 seconds (files took 5.2 seconds to load)
1 example, 0 failures
ポイントシステム(3) -- timecop
timecop
- 時間の経過に関連する仕様のテストで使う。
- Time.currentやDate.todayが返す値を一時的に変更してくれる。
- Timecop.freeze:現在時刻の移動
仕様
- ユーザーがログインに成功すると、「ログインポイント」としてユーザーに1ポイント与えられる。
- ただし、「ログインポイント」はユーザーごとに1日1回しか与えられない。日の区切りは日本時間午前5時とする。
- また、土曜日にログインすると、通常の「ログインポイント」の他に「土曜ログインボーナス」として2ポイントが与えられる。
- specの追加
diff --git a/spec/models/customer_spec.rb b/spec/models/customer_spec.rb
index 6b753c1..e4b767d 100644
--- a/spec/models/customer_spec.rb
+++ b/spec/models/customer_spec.rb
@@ -111,6 +111,29 @@ describe Customer, '.authenticate' do
Customer.authenticate(customer.username, 'correct_password')
}.to change { customer.points }.by(1)
end
+
+ example '日付変更時刻をまたいで2回ログインすると、ユーザーの保有ポイントが2増える' do
+ Time.zone = 'Tokyo'
+ date_boundary = Time.zone.local(2015, 9, 27, 5, 0, 0)
+ expect {
+ Timecop.freeze(date_boundary.advance(seconds: -1))
+ binding.pry
+ Customer.authenticate(customer.username, 'correct_password')
+ Timecop.freeze(date_boundary)
+ Customer.authenticate(customer.username, 'correct_password')
+ }.to change { customer.points }.by(2)
+ end
+
+ example '日付変更時刻をまたがずに2回ログインしても、ユーザーの保有ポイントは1しか増えない' do
+ Time.zone = 'Tokyo'
+ date_boundary = Time.zone.local(2015, 9, 27, 5, 0, 0)
+ expect {
+ Timecop.freeze(date_boundary)
+ Customer.authenticate(customer.username, 'correct_password')
+ Timecop.freeze(date_boundary)
+ Customer.authenticate(customer.username, 'correct_password')
+ }.to change { customer.points }.by(1)
+ end
end
describe Customer, '#points' do
- テスト
1 || Run options: include {:locations=>{"./spec/models/customer_spec.rb"=>[127]}}
2 ||
3 || Customer.authenticate
4 || 日付変更時刻をまたがずに2回ログインしても、ユーザーの保有ポイントは1しか増えない (FAILED - 1)
5 ||
6 || Failures:
7 ||
8 spec/models/customer_spec.rb|130 error| Failure/Error: expect { expected result to have changed by 1, but was changed by 2
9 ||
10 || Finished in 0.33854 seconds (files took 2.73 seconds to load)
11 || 1 example, 1 failure
12 ||
13 || Failed examples:
14 ||
15 || rspec ./spec/models/customer_spec.rb:127 # Customer.authenticate 日付変更時刻をまたがずに2回ログインしても、ユーザーの保有ポイントは1しか増えない
- 実装
diff --git a/app/models/customer.rb b/app/models/customer.rb
index b6c4766..5cb51ba 100644
--- a/app/models/customer.rb
+++ b/app/models/customer.rb
@@ -33,7 +33,19 @@ class Customer < ActiveRecord::Base
def self.authenticate(username, password)
customer = find_by_username(username)
if customer.try(:password_digest) && BCrypt::Password.new(customer.password_digest) == password
- customer.rewards.create(points: 1)
+ Time.zone = 'Tokyo'
+ now = Time.current
+ if now.hour < 5
+ time0 = now.yesterday.midnight.advance(hours: 5)
+ time1 = now.midnight.advance(hours: 5)
+ else
+ time0 = now.midnight.advance(hours: 5)
+ time1 = now.tomorrow.midnight.advance(hours: 5)
+ end
+
+ unless customer.rewards.where(created_at: time0...time1).exists?
+ customer.rewards.create(points: 1)
+ end
customer
else
nil
ポイントシステム(4) -- サービスオブジェクト
プライベートメソッドとして分離
diff --git a/app/models/customer.rb b/app/models/customer.rb
index 5cb51ba..a3c9738 100644
--- a/app/models/customer.rb
+++ b/app/models/customer.rb
@@ -33,22 +33,27 @@ class Customer < ActiveRecord::Base
def self.authenticate(username, password)
customer = find_by_username(username)
if customer.try(:password_digest) && BCrypt::Password.new(customer.password_digest) == password
- Time.zone = 'Tokyo'
- now = Time.current
- if now.hour < 5
- time0 = now.yesterday.midnight.advance(hours: 5)
- time1 = now.midnight.advance(hours: 5)
- else
- time0 = now.midnight.advance(hours: 5)
- time1 = now.tomorrow.midnight.advance(hours: 5)
- end
-
- unless customer.rewards.where(created_at: time0...time1).exists?
- customer.rewards.create(points: 1)
- end
+ grant_login_points_to(customer)
customer
else
nil
end
end
+
+ private
+ def grant_login_points_to(customer)
+ Time.zone = 'Tokyo'
+ now = Time.current
+ if now.hour < 5
+ time0 = now.yesterday.midnight.advance(hours: 5)
+ time1 = now.midnight.advance(hours: 5)
+ else
+ time0 = now.midnight.advance(hours: 5)
+ time1 = now.tomorrow.midnight.advance(hours: 5)
+ end
+
+ unless customer.rewards.where(created_at: time0...time1).exists?
+ customer.rewards.create(points: 1)
+ end
+ end
end
サービスオブジェクトを利用
- serviceの生成
% mkdir app/services
% vi app/services/reward_service.rb
- ソースコードの修正
[devnote@hooni:~/documents/study/oiax] % git --no-pager diff --cached
diff --git a/app/models/customer.rb b/app/models/customer.rb
index 5cb51ba..cbef2b9 100644
--- a/app/models/customer.rb
+++ b/app/models/customer.rb
@@ -33,19 +33,7 @@ class Customer < ActiveRecord::Base
def self.authenticate(username, password)
customer = find_by_username(username)
if customer.try(:password_digest) && BCrypt::Password.new(customer.password_digest) == password
- Time.zone = 'Tokyo'
- now = Time.current
- if now.hour < 5
- time0 = now.yesterday.midnight.advance(hours: 5)
- time1 = now.midnight.advance(hours: 5)
- else
- time0 = now.midnight.advance(hours: 5)
- time1 = now.tomorrow.midnight.advance(hours: 5)
- end
-
- unless customer.rewards.where(created_at: time0...time1).exists?
- customer.rewards.create(points: 1)
- end
+ RewardService.new(customer).grant_login_points
customer
else
nil
diff --git a/app/services/reward_service.rb b/app/services/reward_service.rb
new file mode 100644
index 0000000..a73e450
--- /dev/null
+++ b/app/services/reward_service.rb
@@ -0,0 +1,23 @@
+class RewardService
+ attr_accessor :customer
+
+ def initialize(customer)
+ self.customer = customer
+ end
+
+ def grant_login_points
+ Time.zone = 'Tokyo'
+ now = Time.current
+ if now.hour < 5
+ time0 = now.yesterday.midnight.advance(hours: 5)
+ time1 = now.midnight.advance(hours: 5)
+ else
+ time0 = now.midnight.advance(hours: 5)
+ time1 = now.tomorrow.midnight.advance(hours: 5)
+ end
+
+ unless customer.rewards.where(created_at: time0...time1).exists?
+ customer.rewards.create(points: 1)
+ end
+ end
+end
- テスト
devnote@hooni:~/documents/study/oiax] % bin/rspec spec/models/customer_spec.rb:83
Run options: include {:locations=>{"./spec/models/customer_spec.rb"=>[83]}}
Customer.authenticate
ユーザー名とパスワードに該当するCustomerオブジェクトを返す
パスワードが一致しない場合nilを返す
該当するユーザー名が存在しない場合はnilを返す
パスワード未設定のユーザーを拒絶する
ログインに成功すると、ユーザーの保有ポイントが1増える
日付変更時刻をまたいで2回ログインすると、ユーザーの保有ポイントが2増える
日付変更時刻をまたがずに2回ログインしても、ユーザーの保有ポイントは1しか増えない
Finished in 1.34 seconds (files took 2.7 seconds to load)
7 examples, 0 failures
- specの修正
[devnote@hooni:~/documents/study/oiax] % git --no-pager diff --cached
diff --git a/spec/models/customer_spec.rb b/spec/models/customer_spec.rb
index d85b30a..a1dfa80 100644
--- a/spec/models/customer_spec.rb
+++ b/spec/models/customer_spec.rb
@@ -111,28 +111,6 @@ describe Customer, '.authenticate' do
Customer.authenticate(customer.username, 'correct_password')
}.to change { customer.points }.by(1)
end
-
- example '日付変更時刻をまたいで2回ログインすると、ユーザーの保有ポイントが2増える' do
- Time.zone = 'Tokyo'
- date_boundary = Time.zone.local(2015, 9, 27, 5, 0, 0)
- expect {
- Timecop.freeze(date_boundary.advance(seconds: -1))
- Customer.authenticate(customer.username, 'correct_password')
- Timecop.freeze(date_boundary)
- Customer.authenticate(customer.username, 'correct_password')
- }.to change { customer.points }.by(2)
- end
-
- example '日付変更時刻をまたがずに2回ログインしても、ユーザーの保有ポイントは1しか増えない' do
- Time.zone = 'Tokyo'
- date_boundary = Time.zone.local(2015, 9, 27, 5, 0, 0)
- expect {
- Timecop.freeze(date_boundary)
- Customer.authenticate(customer.username, 'correct_password')
- Timecop.freeze(date_boundary)
- Customer.authenticate(customer.username, 'correct_password')
- }.to change { customer.points }.by(1)
- end
end
describe Customer, '#points' do
diff --git a/spec/services/reward_service_spec.rb b/spec/services/reward_service_spec.rb
new file mode 100644
index 0000000..489c695
--- /dev/null
+++ b/spec/services/reward_service_spec.rb
@@ -0,0 +1,28 @@
+require 'rails_helper'
+
+describe RewardService, '#grant_login_points' do
+ let(:customer) { create(:customer) }
+
+ example '日付変更時刻をまたいで2回ログインすると、ユーザーの保有ポイントが2増える' do
+ Time.zone = 'Tokyo'
+ date_boundary = Time.zone.local(2015, 9, 27, 5, 0, 0)
+ expect {
+ Timecop.freeze(date_boundary.advance(seconds: -1))
+ RewardService.new(customer).grant_login_points
+ Timecop.freeze(date_boundary)
+ RewardService.new(customer).grant_login_points
+ }.to change { customer.points }.by(2)
+ end
+
+ example '日付変更時刻をまたがずに2回ログインしても、ユーザーの保有ポイントは1しか増えない' do
+ Time.zone = 'Tokyo'
+ date_boundary = Time.zone.local(2015, 9, 27, 5, 0, 0)
+ expect {
+ Timecop.freeze(date_boundary)
+ RewardService.new(customer).grant_login_points
+ Timecop.freeze(date_boundary)
+ RewardService.new(customer).grant_login_points
+ }.to change { customer.points }.by(1)
+ end
+end
ポイントシステム(5) -- サービスオブジェクト
- ReceptionService
[devnote@hooni:~/documents/study/oiax] % git --no-pager diff --cached
diff --git a/app/controllers/sessions_controller.rb b/app/controllers/sessions_controller.rb
index 1374715..13ba6ce 100644
--- a/app/controllers/sessions_controller.rb
+++ b/app/controllers/sessions_controller.rb
@@ -1,6 +1,6 @@
class SessionsController < ApplicationController
def create
- if customer = Customer.authenticate(params[:username], params[:password])
+ if customer = ReceptionService.new(params[:username], params[:password]).sign_in
session[:customer_id] = customer.id
else
flash.alert = 'ユーザー名またはパスワードが正しくありません。'
diff --git a/app/models/customer.rb b/app/models/customer.rb
index cbef2b9..f7809f2 100644
--- a/app/models/customer.rb
+++ b/app/models/customer.rb
@@ -29,14 +29,4 @@ class Customer < ActiveRecord::Base
def points
rewards.sum(:points)
end
-
- def self.authenticate(username, password)
- customer = find_by_username(username)
- if customer.try(:password_digest) && BCrypt::Password.new(customer.password_digest) == password
- RewardService.new(customer).grant_login_points
- customer
- else
- nil
- end
- end
end
diff --git a/app/services/reception_desk_service.rb b/app/services/reception_desk_service.rb
new file mode 100644
index 0000000..66547b3
--- /dev/null
+++ b/app/services/reception_desk_service.rb
@@ -0,0 +1,18 @@
+class ReceptionService
+ attr_accessor :username, :password
+
+ def initialize(username, password)
+ self.username = username
+ self.password = password
+ end
+
+ def sign_in
+ customer = Customer.find_by_username(username)
+ if customer.try(:password_digest) && BCrypt::Password.new(customer.password_digest) == password
+ RewardService.new(customer).grant_login_points
+ customer
+ else
+ nil
+ end
+ end
+end
diff --git a/spec/models/customer_spec.rb b/spec/models/customer_spec.rb
index d85b30a..57db71e 100644
--- a/spec/models/customer_spec.rb
+++ b/spec/models/customer_spec.rb
@@ -80,7 +80,7 @@ describe Customer, 'password=' do
end
end
-describe Customer, '.authenticate' do
+xdescribe Customer, '.authenticate' do
let(:customer) {create(:customer, username: 'taro', password: BCrypt::Password.create('correct_password'))}
example 'ユーザー名とパスワードに該当するCustomerオブジェクトを返す' do
diff --git a/spec/services/reception_desk_service_spec.rb b/spec/services/reception_desk_service_spec.rb
new file mode 100644
index 0000000..f79ba10
--- /dev/null
+++ b/spec/services/reception_desk_service_spec.rb
@@ -0,0 +1,34 @@
+require 'rails_helper'
+
+describe ReceptionService, '#sign_in' do
+ let(:customer) {create(:customer, username: 'taro', password: BCrypt::Password.create('correct_password'))}
+
+ example 'ユーザー名とパスワードに該当するCustomerオブジェクトを返す' do
+ result = ReceptionService.new(customer.username, 'correct_password').sign_in
+ expect(result).to eq(customer)
+ end
+
+ example 'パスワードが一致しない場合nilを返す' do
+ result = ReceptionService.new(customer.username, 'wrong_password').sign_in
+ expect(result).to be_nil
+ end
+
+ example '該当するユーザー名が存在しない場合はnilを返す' do
+ result = ReceptionService.new('hanako', 'any_string').sign_in
+ expect(result).to be_nil
+ end
+
+ example 'パスワード未設定のユーザーを拒絶する' do
+ customer.update_column(:password_digest, nil)
+ result = ReceptionService.new(customer.username, '').sign_in
+ expect(result).to be_nil
+ end
+
+ example 'ログインに成功すると、ユーザーの保有ポイントが1増える' do
+ # pending 'Customer#pointsが未実装'
+ # allow(customer).to receive(:points).and_return(0)
+ expect {
+ ReceptionService.new(customer.username, 'correct_password').sign_in
+ }.to change { customer.points }.by(1)
+ end
+end