継継継継継継継継継継継継継 継継 継 2013/6/19
継承を用いたコードのテスト
小林 裕
2013/6/19
目次• 前回より
– 役割のテストを使ったテストダブルの検証
• 継承されたコードのテスト– 継承インターフェース– サブクラスの責任– 固有な動作のテスト
• さいごに2013/6/19 2
役割テストを使ったテストダブルの検証
2013/6/19 3
疑問class DiameterDouble def diameter 10 endend
class GearTest < MiniTest::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) endend
2013/6/19 4
Diameterizable のインターフェースは ‘ width’ に変更されたが、これらテストダブルと Gear は ‘ diameter’ から変更していない
テストを通過しなくなるのでは?
テストのモジュール化class WheelTest < MiniTest::Unit::TestCase include DiameterizableInterfaceTest
def setup @wheel = Wheel.new(26, 1.5) end def test_implements_the_diameterizable_interface assert_respond_to(@wheel, :width) end def test_calculates_diameter # ... endend
2013/6/19 5
この部分をモジュール化
リファクタリング• プログラムの振る舞いを変えることなく、
ソースコードを整理すること
2013/6/19 6
テストダブルの検証 (1/2)
2013/6/19 7
class DiameterDouble def diameter 10 endend
class DiameterDoubleTest < MiniTest::Unit::TestCase include DiameterizableInterfaceTest
def setup @object = DiameterDouble.new endend
class GearTest < MiniTest::Unit::TestCase # ...end
DiameterDoubleTest FAIL test_implements_the_diameterizablie_interface Expected #<DiameterDouble:…> (DiameterDouble) to respond to #width.GearTest PASS test_calculates_gear_inches
width に変更
テストダブルの検証 (2/2)
2013/6/19 8
class DiameterDouble def width 10 endend
class DiameterDoubleTest < MiniTest::Unit::TestCase include DiameterizableInterfaceTest
def setup @object = DiameterDouble.new endend
class GearTest < MiniTest::Unit::TestCase # ...end
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 ratio * wheel.width end
#...end
テストダブルの検証• 役割に対して共有可能なテストモジュール
2013/6/19 9
継承されたコードのテスト
2013/6/19 10
スーパークラスclass Bicycle attr_reader :size, :chain, :tire_size def initialize(args={}) @size = args[:size] @chain = args[:chain] || default_chain @tire_size = args[:tire_size] || default_tire_size post_initialize(args) end
def spares { tire_size: tire_size, chain: chain }.merge(local_spares) end
def default_tire_size raise NotImplementedError end
# サブクラスでオーバーライドされる可能性がある def post_initialize(args) nil end
def local_spares {} end
def default_chain ‘10-speed’ endend
2013/6/19 11
サブクラスclass RoadBike < Bicycle attr_reader :tape_color
def post_initialize(args) @tape_color = args[:tape_color] end
def local_spares {tape_color: tape_color} end
def default_tire_size ‘23’ endend
2013/6/19 12
ヒエラルキー内のすべてのオブジェクトをリスコフの置換原則に従って生成する
ヒエラルキー内のすべてのオブジェクト間で共通のテストが共有可能
共通インターフェース (1/2)module BycycleInterfaceTest def test_responds_to_default_tire_size assert_respond_to (@object, :default_tire_size) end
def test_responds_to_default_chain assert_respond_to (@object, :default_chain) end
def test_responds_to_chain assert_respond_to(@object, :chain) end
def test_responds_to_size assert_respond_to(@object, :size) end
def test_responds_to_tire_size assert_respond_to(@object, :tire_size) end
def test_responds_to_spares assert_respond_to(@object, :spares) endend
2013/6/19 13
共通インターフェース (2/2)class BicycleTest < MiniTest::Unit::TestCase include BicycleInterfaceTest def setup @bike = @object = Bicycle.new({tire_size: 0}) endend
class RoadBikeTest < MiniTest::Unit::TestCase include BicycleInterfaceTest def setup @bike = @object = RoadBike.new endend
2013/6/19 14
BicycleTest PASS test_responds_to_default_chain PASS test_responds_to_tire_size PASS test_responds_to_chain PASS test_responds_to_spares PASS test_responds_to_default_tire_size
RoadBikeTest PASS test_responds_to_default_chain PASS test_responds_to_tire_size PASS test_responds_to_chain PASS test_responds_to_spares PASS test_responds_to_default_tire_size
サブクラスの責任• サブクラスの動作確認• スーパークラスによる強制
2013/6/19 15
サブクラスの動作確認 (1/2)module BicycleSubclassTest def test_responds_to_post_initialize assert_respond_to(@object, :post_initialize) end def test_responds_to_local_spares assert_respond_to(@object, :local_spares) end def test_responds_to_default_tire_size assert_respond_to(@object, :default_tire_size) endend
継承するかは自由
サブクラスによって実行
2013/6/19 16
サブクラスの動作確認 (2/2)class RoadBikeTest < MiniTest::Unit::TestCase include BicycleInterfaceTest include BicycleSubclassTest def setup @bike = @object = RoadBike.new endend
class MountainBikeTest < MiniTest::Unit::TestCase include BicycleInterfaceTest include BicycleSubclassTest def setup @bike = @object = MountainBike.new endend
2013/6/19 17
RoadBikeTest PASS test_responds_to_default_tire_size PASS test_responds_to_spares PASS test_responds_to_chain PASS test_responds_to_post_initialize PASS test_responds_to_local_spares PASS test_responds_to_size PASS test_responds_to_tire_size PASS test_responds_to_default_chain
併せてインクルードすることで正しくサブクラスを生成することが可能
スーパークラスによる強制class BicycleTest < MiniTest::Unit::TestCase include BicycleInterfaceTest
def setup @bike = @object = RoadBike.new({tire_size: 0}) end def test_forces_subclasses_to_implement_default_tire_size assert_raises(NotImplementedError) {@bike.default_tire_size} endend
2013/6/19 18
BicycleTest PASS test_responds_to_default_tire_size PASS test_responds_to_size PASS test_responds_to_default_chain PASS test_responds_to_tire_size PASS test_responds_to_chain PASS test_responds_to_spares PASS test_forces_subclasses_to_implement_default_tire_size
固有な動作のテスト• サブクラスにおける動作のテスト• スーパークラスにおける動作のテスト
2013/6/19 19
サブクラスにおける動作のテストclass RoadBikeTest < MiniTest::Unit::TestCase include BicycleInterfaceTest include BicycleSubclassTest def setup @bike = @object = RoadBike.new({tape_color: 'red‘}) end def test_puts_tape_color_in_local_spares assert_equal 'red', @bike.local_spares[:tape_color] endend
2013/6/19 20
RoadBikeTest PASS test_responds_to_default_chain PASS test_responds_to_default_tire_size PASS test_puts_tape_color_in_local_spares PASS test_responds_to_spares PASS test_responds_to_size PASS test_responds_to_local_spares PASS test_responds_to_post_initialize PASS test_responds_to_tire_size PASS test_responds_to_chain
スーパークラスにおける動作のテストclass StubbedBike < Bicycle def default_tire_size 0 end def local_spares {saddle: 'painful'} endend
class BicycleTest < MiniTest::Unit::TestCase include BicycleInterfaceTest
def setup @bike = @object = Bicycle.new({tire_size: 0}) @stubbed_bike = StubbedBike.new end
def test_forces_subclasses_to_implement_default_tire_size assert_raises(NotImplementedError) { @bike.default_tire_size} end def test_includes_local_spares_in_spares assert_equal @stubbed_bike.spares { tire_size: 0, chain: '10-speed', saddle: 'painful'} endend
2013/6/19 21
BicycleTest PASS test_responds_to_spares PASS test_responds_to_tire_size PASS test_responds_to_default_chain PASS test_responds_to_default_tire_size PASS test_forces_subclasses_to_implement_default_tire_size PASS test_responds_to_chain PASS test_includes_local_spares_in_spares PASS test_responds_to_size
さいごに• テストは不可欠• アプリケーションは高レベルで抽象化
– 変更に対して安全性のテストが必要• テストは一度、適切な場所に
• 依存関係の管理• インターフェースの生成• Persist, Practice, Experiment, Imagine2013/6/19 22
ResponsibilitiesDependencies
InterfacesDucks
InheritanceBehavior sharing
CompositionTesting
ありがとうございました
2013/6/19 23