3. 停車計費程序 Part 2
3-1 目標
接下來實作有註冊的登入用戶,可以選擇「短期費率」或「長期費率」:
- 短期費率:第一個小時 ¥2,之後每半小時 ¥0.5
- 長期費率:一天 ¥16,若停六小時以內 ¥12
3-2 裝 Devise 產生 User Model
編輯 Gemfile
加上 gem "devise"
執行 bundle
,然後重啟伺服器
執行 rails g devise:install
執行 rails g devise user
執行 rake db:migrate
編輯 app/views/layout/application.html.erb
,插入:
<body>
+ <% if flash[:notice] %>
+ <%= flash[:notice] %>
+ <% end %>
+ <% if flash[:alert] %>
+ <%= flash[:alert] %>
+ <% end %>
+ <% if current_user %>
+ <%= link_to('登出', destroy_user_session_path, :method => :delete) %>
+ <%= link_to('修改密碼', edit_registration_path(:user)) %>
+ <% else %>
+ <%= link_to('註冊', new_registration_path(:user)) %> |
+ <%= link_to('登入', new_session_path(:user)) %>
+ <% end %>
...(略)
編輯 app/models/user.rb
,加上 parkings 關聯
class User < ApplicationRecord
+ has_many :parkings
...(略)
編輯 app/models/parking.rb
,加上 user 關聯
class Parking < ApplicationRecord
+ belongs_to :user, :optional => true
...(略)
重啟 Rails 伺服器
3-3 修正前臺接口
修改 parkings_controller.rb
def create
- @parking = Parking.new( :parking_type => "guest", :start_at => Time.now )
+ @parking = Parking.new( :start_at => Time.now )
+
+ # 有登入的話,根據用戶選的費率。沒有登入的話,指定是 guest 費率
+ if current_user
+ @parking.parking_type = params[:parking][:parking_type]
+ @parking.user = current_user
+ else
+ @parking.parking_type = "guest"
+ end
+
修改 app/views/parkings/new.html.erb
<%= form_for @parking do |f| %>
+ <% if current_user %>
+
+ <label>
+ <%= f.radio_button :parking_type, "short-term" %> 短期費率
+ </label>
+
+ <label>
+ <%= f.radio_button :parking_type, "long-term" %> 長期費率
+ </label>
+
+ <% else %>
+ 一般費率
+ <% end %>
+
<p><%= f.submit "開始計費" %></p>
<% end %>
修改 app/views/parkings/show.html.erb
,多加一行顯示是哪一種費率:
<% if @parking.amount.present? # 已經結束,顯示停車費 %>
+ <p>計費方式 <%= @parking.parking_type %></p>
# (略)
實際註冊和登入之後,就可以操作看看了:
3-4 準備「短期費率」測試案例
和準備「一般費率」測試案例一樣,讓我們列出「短期費率」需要測試的清單:
時間長度 | 總金額 |
---|---|
30 分鐘 | 2 |
60 分鐘 | 2 |
61 分鐘 | 2.5 |
90 分鐘 | 2.5 |
120 分鐘 | 3 |
3-5 開始撰寫「短期費率」的測試
- context “short-term” do 現在
calculate_amount
方法裡面有兩種計費方式,我們可以用context
語法來分類,一個區塊是context "guest"
,另一個區塊是"context "short-term
。
describe ".calculate_amount" do
+ context "guest" do
# (略,本來的一般費率測試放在這層)
+ end
+
+ context "short-term" do
+ it "30 mins should be ¥2" do
+ t = Time.now
+ parking = Parking.new( :parking_type => "short-term",
+ :start_at => t, :end_at => t + 30.minutes )
+ parking.user = User.create(:email => "[email protected]", :password => "123455678")
+
+ parking.calculate_amount
+ expect(parking.amount).to eq(200)
+ end
+
+ it "60 mins should be ¥2" do
+ t = Time.now
+ parking = Parking.new( :parking_type => "short-term",
+ :start_at => t, :end_at => t + 60.minutes )
+ parking.user = User.create(:email => "[email protected]", :password => "123455678")
+
+ parking.calculate_amount
+ expect( parking.amount ).to eq(200)
+ end
+
+ it "61 mins should be ¥2.5" do
+ t = Time.now
+ parking = Parking.new( :parking_type => "short-term",
+ :start_at => t, :end_at => t + 61.minutes )
+ parking.user = User.create(:email => "[email protected]", :password => "123455678")
+
+ parking.calculate_amount
+
+ expect( parking.amount ).to eq(250)
+ end
+
+ it "90 mins should be ¥2.5" do
+ t = Time.now
+ parking = Parking.new( :parking_type => "short-term",
+ :start_at => t, :end_at => t + 90.minutes )
+ parking.user = User.create(:email => "[email protected]", :password => "123455678")
+
+ parking.calculate_amount
+ expect( parking.amount ).to eq(250)
+ end
+
+ it "120 mins should be ¥3" do
+ t = Time.now
+ parking = Parking.new( :parking_type => "short-term",
+ :start_at => t, :end_at => t + 120.minutes )
+ parking.user = User.create(:email => "[email protected]", :password => "123455678")
+
+ parking.calculate_amount
+ expect( parking.amount ).to eq(300)
+ end
+
+ end
end
context 和 describe 作用一樣,單純只是分類組織而已,沒有實際作用。通常意思是區分不同情境。
執行 rspec spec/models/parking_spec.rb
測試失敗。
3-6 增加實作
一般費率和短期費率,只有差在停了一小時後,前者是每半小時 ¥1,後者是 ¥0.5,我們很快地就修好實作了:
def calculate_amount
+ factor = (self.user.present?)? 50 : 100
if self.amount.blank? && self.start_at.present? && self.end_at.present?
if duration <= 60
self.amount = 200
else
- self.amount = 200 + ((duration - 60).to_f / 30).ceil * 100
+ self.amount = 200 + ((duration - 60).to_f / 30).ceil * factor
end
end
end
執行 rspec spec/models/parking_spec.rb
全部測試通過。
3-7 重構測試碼
除了實作代碼,測試代碼也可以重構。不同案例之間如果有重復用到的變數,可以抽取出來放在 before
區塊:
describe ".calculate_amount" do
+ before do
+ # 把每個測試都會用到的 @time 提取出來,這個 before 區塊會在這個 describe 內的所有測試前執行
+ @time = Time.new(2017,3, 27, 8, 0, 0) # 固定一個時間比 Time.now 更好,這樣每次跑測試才能確保一樣的結果
+ end
context "guest" do
+ before do
+ # 把每個測試都會用到的 @parking 提取出來,這個 before 區塊會在這個 context 內的所有測試前執行
+ @parking = Parking.new( :parking_type => "guest", :user => @user, :start_at => @time )
+ end
it "30 mins should be ¥2" do
- t = Time.now
- parking = Parking.new(:parking_type => "guest", :start_at => t, :end_at => t + 30.minutes)
- parking.calculate_amount
- expect(parking.amount).to eq(200)
+ @parking.end_at = @time + 30.minutes
+ @parking.calculate_amount
+ expect(@parking.amount).to eq(200)
end
it "60 mins should be ¥2" do
- t = Time.now
- parking = Parking.new(:parking_type => "guest", :start_at => t, :end_at => t + 60.minutes)
- parking.calculate_amount
- expect( parking.amount ).to eq(200)
+ @parking.end_at = @time + 60.minutes
+ @parking.calculate_amount
+ expect( @parking.amount ).to eq(200)
end
it "61 mins should be ¥3" do
- t = Time.now
- parking = Parking.new(:parking_type => "guest", :start_at => t, :end_at => t + 61.minutes)
- parking.calculate_amount
- expect( parking.amount ).to eq(300)
+ @parking.end_at = @time + 61.minutes
+ @parking.calculate_amount
+ expect( @parking.amount ).to eq(300)
end
it "90 mins should be ¥3" do
- t = Time.now
- parking = Parking.new(:parking_type => "guest", :start_at => t, :end_at => t + 90.minutes)
- parking.calculate_amount
- expect( parking.amount ).to eq(300)
+ @parking.end_at = @time + 90.minutes
+ @parking.calculate_amount
+ expect( @parking.amount ).to eq(300)
end
it "120 mins should be ¥4" do
- t = Time.now
- parking = Parking.new(:parking_type => "guest", :start_at => t, :end_at => t + 120.minutes)
- parking.calculate_amount
- expect( parking.amount ).to eq(400)
+ @parking.end_at = @time + 120.minutes
+ @parking.calculate_amount
+ expect( @parking.amount ).to eq(400)
end
end
context "short-term" do
+ before do
+ # 把每個測試都會用到的 @user 和 @parking 提取出來
+ @user = User.create( :email => "[email protected]", :password => "123455678")
+ @parking = Parking.new( :parking_type => "short-term", :user => @user, :start_at => @time )
+ end
it "30 mins should be ¥2" do
- t = Time.now
- parking = Parking.new( :parking_type => "short-term",
- :start_at => t, :end_at => t + 30.minutes )
- parking.user = User.create( :email => "[email protected]", :password => "123455678")
- parking.calculate_amount
- expect(parking.amount).to eq(200)
+ @parking.end_at = @time + 30.minutes
+ @parking.calculate_amount
+ expect(@parking.amount).to eq(200)
end
it "60 mins should be ¥2" do
- t = Time.now
- parking = Parking.new( :parking_type => "short-term",
- :start_at => t, :end_at => t + 60.minutes )
- parking.user = User.create( :email => "[email protected]", :password => "123455678")
- parking.calculate_amount
- expect( parking.amount ).to eq(200)
+ @parking.end_at = @time + 60.minutes
+ @parking.calculate_amount
+ expect( @parking.amount ).to eq(200)
end
it "61 mins should be ¥2.5" do
- t = Time.now
- parking = Parking.new( :parking_type => "short-term",
- :start_at => t, :end_at => t + 61.minutes )
- parking.user = User.create( :email => "[email protected]", :password => "123455678")
- parking.calculate_amount
- expect( parking.amount ).to eq(250)
+ @parking.end_at = @time + 61.minutes
+ @parking.calculate_amount
+ expect( @parking.amount ).to eq(250)
end
it "90 mins should be ¥2.5" do
- t = Time.now
- parking = Parking.new( :parking_type => "short-term",
- :start_at => t, :end_at => t + 90.minutes )
- parking.user = User.create( :email => "[email protected]", :password => "123455678")
- parking.calculate_amount
- expect( parking.amount ).to eq(250)
+ @parking.end_at = @time + 90.minutes
+ @parking.calculate_amount
+ expect( @parking.amount ).to eq(250)
end
it "120 mins should be ¥3" do
- t = Time.now
- parking = Parking.new( :parking_type => "short-term",
- :start_at => t, :end_at => t + 120.minutes )
- parking.user = User.create( :email => "[email protected]", :password => "123455678")
- parking.calculate_amount
- expect( parking.amount ).to eq(300)
+ @parking.end_at = @time + 120.minutes
+ @parking.calculate_amount
+ expect( @parking.amount ).to eq(300)
end
end
end
但是,也不能做的太過分,例如把所有測試案例改寫成循環:
# 不好的例子,請不要這麽寫
[[30, 200], [60, 200], [61, 300], [90, 300], [120, 400]].each do |data|
it "#{data[0]} mins should be #{data[1]}" do
@parking.end_at = @time + data[0].minutes
@parking.calculate_amount
expect(@parking.amount).to eq(data[1])
end
end
這樣就太過分了,因為測試碼需要盡量好讀,每一個測試案例盡量清楚,能一眼就看出來要測試什麽。
因此測試代碼比實作還多行是正常的,請耐著性子看完,你會發現其實非常平鋪直敘,就是 1. 建立測試資料 2.執行程序 3. 檢查結果
3-8 擴充長期費率計算
長期費率的計算,跟前兩者差比較多。讓我們來調整看看。來修改 calulate_amount
方法,根據不同費率呼叫不同計算方法:
def calculate_amount
if self.amount.blank? && self.start_at.present? && self.end_at.present?
if self.user.blank?
self.amount = calculate_guest_term_amount # 一般費率
elsif self.parking_type == "long-term"
self.amount = calculate_long_term_amount # 長期費率
elsif self.parking_type == "short-term"
self.amount = calculate_short_term_amount # 短期費率
end
end
end
def calculate_guest_term_amount
if duration <= 60
self.amount = 200
else
self.amount = 200 + ((duration - 60).to_f / 30).ceil * 100
end
end
def calculate_short_term_amount
if duration <= 60
self.amount = 200
else
self.amount = 200 + ((duration - 60).to_f / 30).ceil * 50
end
end
def calculate_long_term_amount
# TODO
end
在跑一次測試,一片綠,非常好!! 這樣修改 calulate_amount 有測試安全網太棒了。
剩下一個計算長期費率的就當作挑戰作業吧。
3-9 如何在測試除錯
在測試代碼中,你可以直接 puts
變數,跑測試的時候就會印出來了。這是最簡單的除錯方式。
例如我們想觀察一下執行 calculate_amount
方法時,裡面的 parking_type
長怎樣:
def calculate_amount
+ puts "----"
+ puts self.parking_type
+ puts "----"
# (略)
執行 rspec spec/models/parking_spec.rb
會發現每個測試都印出來了。
但是這樣可能不是我們除錯時想要的,有沒有辦法只跑我想要除錯的那一個測試案例?
方法一:暫時註解掉其他的測試案例,只留下一個就好了 方法二:請編輯 spec/rails_helper.rb
,加上兩行設定:
RSpec.configure do |config|
# (略)
+ config.filter_run :focus => true
+ config.run_all_when_everything_filtered = true
然後修改 spec/models/parking_spec.rb
針對你想要單獨測試的案例,加上 :focus => true
,例如:
- it "30 mins should be ¥2" do
+ it "30 mins should be ¥2", :focus => true do
@parking.end_at = @time + 30.minutes
@parking.save
expect(@parking.amount).to eq(200)
end
再跑一次測試 rspec spec/models/parking_spec.rb
就只會跑這個測試了。除錯完畢把 :focus => true
移除就可以了。
除了用 puts
,你也可以在用 byebug
下中斷點(這是一個內建的gem,你在Gemfile裡面可以看到),就會在執行到那一行時停下來,可以檢查變數,輸入 continue
就會繼續執行下去。
例如
def calculate_amount
+ byebug
# (略)
執行 rspec spec/models/parking_spec.rb
會發現停在中途,這時候可以檢查變數。
最後輸入 continue
就會繼續執行下去。這一招不只在測試可以用,平常開發除錯也可以使用:rails server
伺服器就會在你下中斷點的地方暫時停止以供除錯。