今天,要來說明鴨子類型的測試,選擇好測試案例後,我們就可以根據前面兩天的步驟來測試輸入及輸出訊息。
以第5章的程式碼為範例,程式碼包含了Preparers
與Trip
之間的合作,它現在可以被當作是一個 Preparable
。
class Mechanic
def prepare_trip(trip)
trip.bicycles.each {|bicycle|
prepare_bicycle(bicycle)}
end
#...
end
class TripCoordinator
def prepare_trip(trip)
buy_food(trip.customers)
end
# ...
end
class Driver
def prepare__trip(trip)
vehicle = trip.vehicle
gas_up(vehicle)
fill_water_tank(vehicle)
end
# ...
end
class Trip
attr_reader :bicycles, :customers, :vehicle
def prepare(preparers)
preparers.each {|preparer|
preparer.prepare_trip(self)}
end
end
PreparerlnterfaceTest
定義為模組,測試便可以只撰寫一次,然後在所有扮演該角色的物件中重複使用。test_implements_the_preparer_interface
方法是用於測試輸入訊息,因此它是屬於接收物件的測試。module PreparerInterfaceTest
def test_implements_the_preparer_interface
assert_respond_to(@object, :prepare_trip)
end
end
class MechanicTest < MiniTest::Unit::TestCase
include PreparerInterfaceTest
def setup
@mechanic = @object = Mechanic.new
end
#其他依賴於©mechanic的測試
end
class TripCoordinatorTest < MiniTest::Uiiit::Testcase
include PreparerInterfaceTest
def setup
@trip_coordinator = @object = Tripcoordinator.new
end
end
class DriverTest < MiniTest::Uiiit::TestCase
include PreparerlnterfaceTest
def setup
@driver = @object = Driver.new
end
end
DriverTest
PASS test_implements_the_preparer_interface
MechanicTest
PASS test_irr^)lements_the_preparer_interface
TripCoordinatorTest
PASS test_in^>lements_the_preparer_interface
證明所有的接收者都正確地實作了prepare.trip
。
現在用昨天學到的測試方法證明Trip
有正確地傳送訊息。
class TripTest < MiniTest::Unit::Testcase
def test_requests_trip_preparation
@preparer = MiniTest::Mock.new
@trip = Trip.new
@preparer.expect(:prepare_trip, nil, [@trip])
@trip.prepare{[@preparer])
@preparer.verify
end
end
TripTest
PASS test_requests_trip_preparation
在先前的「測試輸入訊息」一節裡,介紹了「活在夢中」的問題。而在該節的最終測試裡,有一項應該失敗但卻通過的測試,因為測試替身使用了過時的方法。
將test_implements_the_diameterizable_interface
從Wheel
裡擷取成一個獨立的模組,將撷取出來的行為重新引入到WheelTest
,並使用Wheel
初始化@object
module DiameterizablelnterfaceTest
def test_implements_the_diaineterizable_interface
assert_respond_to(@object, :width)
end
end
class WheelTest < MiniTest: :Uiiit: :Testcase
include DiameterizablelnterfaceTest
def setup
@wheel = @object = Wheel.new(26, 1.5)
end
def test_calculates_diameter
# ...
end
end
WheelTest
PASS test_implements_the_diameterizable_interface
PASS test_calculates_diameter
測試也會有時效性的問題,我們利用這個模組來防止測試替身過時。
class DiameterDouble
def diameter
10
end
end
#證明測試替身遵從了
#這項測試所期望的介面。
class DiameterDoubleTest < MiniTest::Unit::TestCase
include DiameterizablelnterfaceTest
def setup
@object = DiameterDouble.new end
end
end
class GearTest < MiniTost::Unit::TestCase
def test_calculates_gear_inches
gear = Gear.new(
chainring: 52,
cog: 11,
wheel : DiameterDouble. new)
assert_in_delta(47.27,
gear.gear_inches,
0.01)
end
end
DiameterDoubleTest
FAIL test_implements_the_diameterizable_interface
Expected #<DiameterDouble:...> (DiameterDouble) to respond to #width.
GearTest
PASS test_calculates_gear_inches
藉由測試失敗會致使我們修正DiameterDouble
, 去補上width
class DiameterDouble
def width
10
end
end
通過之後又會再撞到下一個錯誤,因為diameter
已經被換成width
,而gear_inches
還未跟上
DiameterDoubleTest
PASS test_implements_the_diameterizable_interface
GearTest
ERROR test_calculates_gear_inches
undefined method 'diameter'
for #<DiameterDouble:0x0000010090a7f8>
gear_test.rb:35:in 'gear_inches'
gear_test.rb:86:in 'test_calculates_gear_inches'
class Gear
def gear_inches
# 終於,「width」 替換成了 「diameter」
ratio* wheel .width
end
# ...
end
DiameterDoubleTest
PASS test_inplements_the_diameterizable_interface 03
GearTest
PASS test_calculates_gear_inches
測試鴨子類型,會產生測試可共用角色的需求。從受測物件的角度來看,其他所有的物件都是角色。視物件為角色的代表,這樣能夠讓應用程式和測試裡的耦合變得鬆散並且提昇靈活性。
實作上就是透過一步一步的修正以通過測試,確認所有改動都是合理且可行的。
參考資料: