Chuck Remes
2008-Mar-25 01:50 UTC
[rspec-users] [Q] how do I test the behavior of a Builder pattern object?
So I have a complex object that I need to construct. This complex object, at runtime, takes an object in its initializer. The initializer interrogates the object and creates several collaborative objects based upon values from that interrogation. The construction is complicated enough that it pretty much begs to be implemented via the Builder Pattern. My problem is that I don''t know how to test this behavior regardless of whether the object handles its own construction or I create a separate Builder object to construct it for me. For example: class ComplexObjectBuilder def initialize @complex_object = ComplexObject.new end def create_foo(control, value) case control do when :a then @complex_object.foo = SpecialObject.new(value * 4) when :v then @complex_object.foo = OtherObject.new when :z then @complex_object.foo = YAObject.new end end def create_bar(control, value) # similar to above end def construct # some business rules to make sure the object is complete; use # defaults for fields that were not set via the interface return @complex_object end end @builder = ComplexObjectBuilder.new @builder.create_foo(params_object.accessor1, params_object.accessor2) @builder.create_bar(params_object.accessor3, params_object.accessor4) baz = @builder.construct How the heck do I test anything here? I do not see how I can validate the behavior of #create_foo or #create_bar without exposing @complex_object via a public interface. Those #create_* methods are purely for construction and do not return a value. Only #construct returns a value which should be the completed object Is there a transformation to consider to make this more testable? I''m hoping someone has tackled this before and can provide me some insight. Many thanks... cr
Chuck Remes
2008-Mar-25 02:49 UTC
[rspec-users] [Q] how do I test the behavior of a Builder pattern object?
On Mar 24, 2008, at 8:50 PM, Chuck Remes wrote:> [snip code] > > How the heck do I test anything here? I do not see how I can validate > the behavior of #create_foo or #create_bar without exposing > @complex_object via a public interface. Those #create_* methods are > purely for construction and do not return a value. Only #construct > returns a value which should be the completed object > > Is there a transformation to consider to make this more testable?Just to show I''m not sitting back and waiting for the answer to be handed to me on a silver platter... I looked through the rspec specs and found a construction that might be useful. In spec/spec/extensions/main_spec.rb we see this: it "should create an Options object" do @main.send(:rspec_options).should be_instance_of(Spec::Runner::Options) @main.send(:rspec_options).should === $rspec_options end In a #before block the @main instance variable was instantiated and a module was included. I''m assuming #send is used to check the value of a private accessor. So I could potentially do something like this: describe ComplexObjectBuilder, "construction" do before(:each) do @builder = ComplexObjectBuilder.new @builder.foo(control1, val1) @builder.bar(control2, val2) end it "should create a YAObject for #foo" do @builder.send(:foo).should be_instance_of(YAObject) end it "should create a ZObject for #bar" do @builder.send(:bar).should be_instance_of(ZObject) end end Is this acceptable? I read through the "specs on private methods" thread and the consensus was this should only be done when there is *no other option*. Is there another option? cr
Pat Maddox
2008-Mar-25 05:04 UTC
[rspec-users] [Q] how do I test the behavior of a Builder pattern object?
On Mon, Mar 24, 2008 at 6:50 PM, Chuck Remes <cremes.devlist at mac.com> wrote:> So I have a complex object that I need to construct. This complex > object, at runtime, takes an object in its initializer. The > initializer interrogates the object and creates several collaborative > objects based upon values from that interrogation. > > The construction is complicated enough that it pretty much begs to be > implemented via the Builder Pattern. > > My problem is that I don''t know how to test this behavior regardless > of whether the object handles its own construction or I create a > separate Builder object to construct it for me. > > For example: > > class ComplexObjectBuilder > def initialize > @complex_object = ComplexObject.new > end > > def create_foo(control, value) > case control do > when :a then @complex_object.foo = SpecialObject.new(value * 4) > when :v then @complex_object.foo = OtherObject.new > when :z then @complex_object.foo = YAObject.new > end > end > > def create_bar(control, value) > # similar to above > end > > def construct > # some business rules to make sure the object is complete; use > # defaults for fields that were not set via the interface > return @complex_object > end > end > > @builder = ComplexObjectBuilder.new > @builder.create_foo(params_object.accessor1, params_object.accessor2) > @builder.create_bar(params_object.accessor3, params_object.accessor4) > baz = @builder.construct > > How the heck do I test anything here? I do not see how I can validate > the behavior of #create_foo or #create_bar without exposing > @complex_object via a public interface. Those #create_* methods are > purely for construction and do not return a value. Only #construct > returns a value which should be the completed object > > Is there a transformation to consider to make this more testable? > > I''m hoping someone has tackled this before and can provide me some > insight. Many thanks... > > crHey Chuck, I would toss whatever code you have and BDD it from scratch. Shouldn''t be that difficult...you can start off with stuff like describe ComplexObjectBuilder, " when foo is built with :a" do before(:each) do builder = ComplexObjectBuilder.new builder.create_foo :a, 3 @built = builder.construct end it "should create a special object" do @built.foo.should be_a(SpecialObject) end it "should quadruple the value passed to the special object" do @built.foo.some_val.should == 12 end end Alternatively, you could mock the calls to the constructed objects. In this case though, that probably leads to specs that are too tightly coupled to the implementation. I know this seems like kind of a cop-out, but you''ll get much better results if you BDD this from scratch. If this is super complex legacy stuff, just try to get some semi-high level tests around it (the sort of test I showed would work fine), and then you can refactor it. Pat
Pat Maddox
2008-Mar-25 05:08 UTC
[rspec-users] [Q] how do I test the behavior of a Builder pattern object?
On Mon, Mar 24, 2008 at 7:49 PM, Chuck Remes <cremes.devlist at mac.com> wrote:> Is this acceptable? I read through the "specs on private methods" > thread and the consensus was this should only be done when there is > *no other option*. Is there another option?There''s always another option, and anybody who says otherwise is flat out wrong. You can *always* test an object via its public interface. If there''s a private method that you want to test, but there''s no way to exercise it by the public interface, then that means the code is not used and should be deleted. Pat
Chuck Remes
2008-Mar-25 14:22 UTC
[rspec-users] [Q] how do I test the behavior of a Builder pattern object?
On Mar 25, 2008, at 12:04 AM, Pat Maddox wrote:> On Mon, Mar 24, 2008 at 6:50 PM, Chuck Remes > <cremes.devlist at mac.com> wrote: >> So I have a complex object that I need to construct. This complex >> object, at runtime, takes an object in its initializer. The >> initializer interrogates the object and creates several collaborative >> objects based upon values from that interrogation. >> >> The construction is complicated enough that it pretty much begs to be >> implemented via the Builder Pattern. >> >> My problem is that I don''t know how to test this behavior regardless >> of whether the object handles its own construction or I create a >> separate Builder object to construct it for me. >> >> For example: >> >> class ComplexObjectBuilder >> def initialize >> @complex_object = ComplexObject.new >> end >> >> def create_foo(control, value) >> case control do >> when :a then @complex_object.foo = SpecialObject.new(value * 4) >> when :v then @complex_object.foo = OtherObject.new >> when :z then @complex_object.foo = YAObject.new >> end >> end >> >> def create_bar(control, value) >> # similar to above >> end >> >> def construct >> # some business rules to make sure the object is complete; use >> # defaults for fields that were not set via the interface >> return @complex_object >> end >> end >> >> @builder = ComplexObjectBuilder.new >> @builder.create_foo(params_object.accessor1, params_object.accessor2) >> @builder.create_bar(params_object.accessor3, params_object.accessor4) >> baz = @builder.construct >> >> How the heck do I test anything here? I do not see how I can >> validate >> the behavior of #create_foo or #create_bar without exposing >> @complex_object via a public interface. Those #create_* methods are >> purely for construction and do not return a value. Only #construct >> returns a value which should be the completed object >> >> Is there a transformation to consider to make this more testable? >> >> I''m hoping someone has tackled this before and can provide me some >> insight. Many thanks... >> >> cr > > Hey Chuck, > > I would toss whatever code you have and BDD it from scratch. > Shouldn''t be that difficult...you can start off with stuff like > > describe ComplexObjectBuilder, " when foo is built with :a" do > before(:each) do > builder = ComplexObjectBuilder.new > builder.create_foo :a, 3 > @built = builder.construct > end > it "should create a special object" do > @built.foo.should be_a(SpecialObject) > end > > it "should quadruple the value passed to the special object" do > @built.foo.some_val.should == 12 > end > end >Pat, thanks for your response. I have question regarding your suggestion. In the first "it" you are asserting that @built.foo should be an object. The @built instance variable is the ComplexObject and not the ComplexObjectBuilder. Shouldn''t we be testing the behavior of the builder and not the ComplexObject the builder produces? Another way, shouldn''t I put the ComplexObject assertions into its own set of specs? It just seems like we''re mixing the behavior of two objects here: ComplexObjectBuilder and ComplexObject. Perhaps that is okay since they are so tightly coupled. In a sense the ComplexObjectBuilder is just a fancy constructor for the other object; it only gets refactored out to its own object because it has so many parts/steps. ?? cr