Link Search Menu Expand Document

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>

    # (略)

實際註冊和登入之後,就可以操作看看了:

image

image

image

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 => "test@example.com", :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 => "test@example.com", :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 => "test@example.com", :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 => "test@example.com", :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 => "test@example.com", :password => "123455678")
+
+       parking.calculate_amount
+       expect( parking.amount ).to eq(300)
+     end
+
+   end
  end

context 和 describe 作用一樣,單純只是分類組織而已,沒有實際作用。通常意思是區分不同情境。

執行 rspec spec/models/parking_spec.rb 測試失敗。

image

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 全部測試通過。

image

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 => "test@example.com", :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 => "test@example.com", :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 => "test@example.com", :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 => "test@example.com", :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 => "test@example.com", :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 => "test@example.com", :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 會發現每個測試都印出來了。

image

但是這樣可能不是我們除錯時想要的,有沒有辦法只跑我想要除錯的那一個測試案例?

方法一:暫時註解掉其他的測試案例,只留下一個就好了 方法二:請編輯 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 移除就可以了。

image

除了用 puts,你也可以在用 byebug 下中斷點(這是一個內建的gem,你在Gemfile裡面可以看到),就會在執行到那一行時停下來,可以檢查變數,輸入 continue 就會繼續執行下去。

例如

   def calculate_amount
+    byebug
     # (略)

執行 rspec spec/models/parking_spec.rb 會發現停在中途,這時候可以檢查變數。

image

最後輸入 continue 就會繼續執行下去。這一招不只在測試可以用,平常開發除錯也可以使用:rails server 伺服器就會在你下中斷點的地方暫時停止以供除錯。

image


Copyright © 2010-2021 Wen-Tien Chang All Rights Reserved.