[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

On Mar 24, 2008, at 8:50 PM, Chuck R. 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

On Mon, Mar 24, 2008 at 7:49 PM, Chuck R. [email protected]
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

On Mar 25, 2008, at 12:04 AM, Pat M. wrote:

My problem is that I don’t know how to test this behavior regardless
def create_foo(control, value)

baz = @builder.construct
I’m hoping someone has tackled this before and can provide me some
before(:each) do
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

On Mon, Mar 24, 2008 at 6:50 PM, Chuck R. [email protected]
wrote:

separate Builder object to construct it for me.
when :a then @complex_object.foo = SpecialObject.new(value * 4)
# some business rules to make sure the object is complete; use
How the heck do I test anything here? I do not see how I can validate
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

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