你好,我是富士大顆 Aiko
今天就拿最近的筆試題目來進行吧!
# app > redis > max_ordianal_number.rb
class MaxOrdinalNumber
EXPIRE_DAY = 1
EXPIRE_SEC = 60 * 60 * 24 * EXPIRE_DAY
KEY_PREFIX = "max_ordinal_number"
def initialize
# ...
end
def get
# ...
end
def set
# ...
end
end
# spec > redis > max_ordinal_number_spec.rb
require "rails_helper"
RSpec.describe MaxOrdinalNumber do
let(:screening_id) { "margin_sell_up" }
let(:redis_key) { "#{described_class::KEY_PREFIX}:#{screening_id}" }
describe "#set and #get" do
let(:max_ordinal_number) { described_class.new(screening_id) }
let(:number) { 5 }
before { max_ordinal_number.set(number) }
it { expect(REDIS.ttl(redis_key)).to eq 86400 } # 60 * 60 * 24 * 1
it { expect(max_ordinal_number.get).to eq 5 }
end
end
所以我先從 _spec.rb
來閱讀,實際需要產出的結果是什麼
require "rails_helper"
RSpec.describe MaxOrdinalNumber do
let(:screening_id) { "margin_sell_up" }
# `let` 在 `rspec` 功能就是宣告變數,而這邊就是宣告 `:screening_id` = `margin_sell_up`
let(:redis_key) { "#{described_class::KEY_PREFIX}:#{screening_id}" }
# 比較複雜,但其實也是一樣的,`redis_key` 由 `KEY_PREFIX` 和 `screening_id` 組成的;
# described_class 是 RSpec 的一個特殊方法,它會回傳目前被描述的類別(現在是 MaxOrdinalNumber)
# 綜合以上,redis_key 可能的值會是 "max_ordinal_number:margin_sell_up"
describe "#set and #get" do
#題目重點喔!因為我們要寫的就是 #set, #get, #initialize
#所以這邊就是讀取 redis 跟寫入 redis 的方法測試
let(:max_ordinal_number) { described_class.new(screening_id) }
# 宣告 `max_ordinal_number` = `MaxOrdinalNumber`.new(附帶 screening_id 做為參數)
let(:number) { 5 }
before { max_ordinal_number.set(number) }
# 在 #set 之前,就把 number 設為 5
it { expect(REDIS.ttl(redis_key)).to eq 86400 } # 60 * 60 * 24 * 1
# .ttl 是 redis 的用法,也就是預期 Redis 中對應的 redis_key 的 TimeToLive(TTL)應為 86400 秒。
it { expect(max_ordinal_number.get).to eq 5 }
# 預期從 redis 拿出的值是 5
end
end
這個測試主要驗證兩件事:
用 VSCode 隨意開一個資料夾,我的話是:/Users/bagel.florence/fn_test
開個檔案:max_ordinal_number_spec.rb
然後把題目貼上檔案去
$gem install rspec
安裝 Rspec
$rspec
& 程式碼修改不知道為什麼用 rspec
log 會說 No examples found.
所以改用直接指定檔名 rspec max_ordinal_number_spec.rb
會出現這樣的錯誤訊息:
表示 Ruby 無法找到名為 MaxOrdinalNumber 的 class
解決的辦法可以是:
由於我還沒新增 max_ordinal_number.rb
所以它找不到,所以我新增,然後貼上題目:
class MaxOrdinalNumber
EXPIRE_DAY = 1
EXPIRE_SEC = 60 * 60 * 24 * EXPIRE_DAY
KEY_PREFIX = "max_ordinal_number"
def initialize
end
def get
end
def set
end
end
並且在 rspec.rb 檔開頭就使用 require_relative 'max_ordinal_number'
(因為他們在同一個資料夾,不然要有完整絕對路徑)
再跑一次 rspec
錯誤不一樣了!
顯示 initialize 方法期望 0 個參數,但實際上你傳遞了 1 個。
這是什麼意思?
對的,你需要在 initialize
方法中接收一個參數 screening_id
。
這樣當 rspec 中使用 described_class.new(screening_id)
new 新的 MaxOrdinalNumber
實體變數時,這個 screening_id
參數就會被傳遞到 initialize
方法。
rb 中的 initialize
目前是空的
沒有任何 block, 也沒有設定要接收任何參數
那我們先完整它:
def initialize(screening_id)
#設定執行時要接收 screening_id 這個參數
@screening_id = screening_id
#要有裝 screening_id 的容器
@redis_key = "#{KEY_PREFIX}:#{@screening_id}"
#同樣的在 rspec 一開始有提到的 redis_key 也要有容器裝
end
這樣,screening_id
會被傳遞到 initialize
方法,並設置為實例變數 @screening_id
。在其他方法(如 get
和 set
)中使用它了。
ok, 繼續 rspec
新的錯誤!至少往下移了:
set 的參數數量不對,set 方法期望 0 個參數,但在測試中你傳遞了 1 個參數(number)
為什麼參數是 number?
因為在 rspec 裡,before { max_ordinal_number.set(number) }
呼叫了 set 方法並傳遞了一個名為 number 的參數。這個 number 參數是由另一個 let(:number) { 5 } 宣告所設定的。因此,set 方法應該接收一個名為 number 的參數。
所以 rb 的 set:
def set(number)
end
給它 number 參數
再繼續 rspec!
新的錯誤~
這指出了兩個問題:
因為題目跟 Redis 有關,該知道的還是要知道!
但 Redis 的文件有夠長,在時間緊要的關係下,要縮小閱讀的範圍...
重新確認下題目的 expect:
it { expect(REDIS.ttl(redis_key)).to eq 86400 } # 60 * 60 * 24 * 1
翻成白話就是:我們期待 REDIS.ttl 會等於 86400 (秒);剩餘過期時間是 24 小時!.ttl
是屬於 Redis 的用法,用於查詢一個已經設定的 key 的 TTL。
這個時間以秒為單位。所以,set 方法的時候,要一起設定 TTL
所以是跟 ttl 有關的,且要存進去 Redis (能查詢就是已經寫進去啦)前,先設定好
在 Redis 中,.setex
方法是用來設定帶有 TTL 的 key-value pair 的。
如果你需要設定一個 key-value pair 而不需要過期時間,你可以使用 .set
方法。
但因為需求中明確要求有過期時間,所以 .setex
是更適合的選擇。
.setex
在 Redis 中設定一個帶有 TTL(以秒為單位)的 key-value pair。
這個方法接受三個參數:
參數必須按照順序
試著把 code 按照說明帶入:
@redis.setex(@redis_key, EXPIRE_SEC, number)
@redis_key
是要設定的 key。EXPIRE_SEC
是 key 的 TTL。的確名稱跟 TTL 的意思一樣number
是與 key 相關聯的 value。這段程式碼的意思是設定了一個 key-value pair 之後,這個 pair 會在 EXPIRE_SEC
秒後自動從 Redis 中刪除。
因為 @redis.setex
這樣寫,也要在 initialize 方法寫個容器裝這個“值”,因此:
@redis = Redis.new
安裝個 redis: $gem install redis
再 rspec
結果問題就變了:
redis 看起來有問題,連不上去
那試試看把 require 'redis'
加到兩個檔案的頭部裡面去吧
...
(後省)
TDD 就是這樣的一個過程!
直到把所有的問題都解決了,測試才剛開始
因為後續還要一起測試相關的整合功能測試
這段過程考驗的是耐性還有“閱讀錯誤”以及解決問題的能力
總之,一起加油吧!
如果你也愛上測試(??
推薦羅伯特大神的 RSpec 系列
你會對測試有更深的理解!
# services/us_market.rb
class UsMarket
# class method 1 判斷現在是否為美股開盤時間,回傳值 type 為 boolean
# class method 2 判斷現在是否為美股收盤時間,回傳值 type 為 boolean
end
# spec/services/us_market_spec.rb
RSpec.describe UsMarket do
# 請補上對應測試,請記得此測試不管任何時間執行都必須要完全正確
end
這次題目的字很多,但不要緊張,我們一段段看:
之前的文章有探討:vol. 19 Rails 裡的 helper, Service Object, Concern 到底怎麼用?
官方文件
timecop 是用在 test 環境的 gem
針對時間計算
主要有三個方法:freeze
travel
scale
freeze
:凍結時間到指定時刻
Timecop.freeze(Date.today + 30) do
assert joe.mortgage_due?
end
travel
:到特定的時間點(不一定是過去可以是未來),時間從那裡可以繼續前進
Timecop.travel(Time.local(2008, 9, 1, 10, 5, 0))
end
Timecop.travel(Time.now + 3.days) do
puts Time.now
# 會顯示未來三天後的時間
end
scale
:加速或減速時間的流逝速度,縮放因子為 1 時,時間流逝正常;縮放因子大於 1 時,時間會加速流逝;縮放因子小於 1 但大於 0 時,時間會減速。
Timecop.scale(3600)
# 縮放因子為 3600(即一小時的秒數,因此每過一秒鐘,Time.now 會報告時間已經過去了一小時
根據題目需求,我們應該只要用到 freeze
關於時間,Ruby 有一整個 Time
的 class 表示時間。這個類別提供了多種方法來操作和查詢時間。以下是一些基本用法:
# 當前時間
current_time = Time.now
# 特定時間(年、月、日、時、分、秒,時區)
specific_time = Time.new(2021, 10, 10, 12, 0, 0, "+09:00")
你可以使用以下方法來獲取時間的不同部分:
time = Time.now
time.year # 年
time.month # 月
time.day # 日
time.hour # 小時
time.min # 分鐘
time.sec # 秒
time.wday # 星期的第幾天(0 是星期日,1 是星期一,等等)
Ruby 的 Time
類別也可以進行基本時間運算:
# 增加時間
future_time = Time.now + 3600 # 現在時間加上 3600 秒(1 小時)
# 減少時間
past_time = Time.now - 3600 # 現在時間減去 3600 秒(1 小時)
如果你需要做時區轉換,Ruby 的 Time
類別可能不是最方便的選擇。但目前依據題目的需求,Time 很夠用惹!
處理更複雜的時區問題,例如一直換時區,或是有日光節約時間,則可以用 TZInfo
。
require 'tzinfo'
# 得到紐約時間
ny_time = TZInfo::Timezone.get('America/New_York').now
Time
類別提供了一個 strftime
方法,能以不同格式顯示時間:
time = Time.now
formatted_time = time.strftime("%Y-%m-%d %H:%M:%S")
Time
類別有很多其他功能和方法,可以參考 Ruby 官方文件 。
在這題,Time
可以搭配 in_time_zone
使用