LoginSignup
3
3

More than 5 years have passed since last update.

勉強会BDD偏<4>RSpecとCapybara - ポイントシステム

Last updated at Posted at 2016-03-09

関連記事

ポイントシステム(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
3
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
3