Wednesday, May 31, 2006

Use class methods for stubs

Since I believe in unit testing one concrete class at a time I frequently use stubs in my tests. These stubs always return the same value and are unimportant to the current test.

For example, assume you have an Employee class that contains check_email behavior.
class Employee
def initialize(cell_phone, laptop)
@cell_phone = cell_phone
@laptop = laptop
end

def check_email(user, password)
if @laptop.working?
@laptop.check_email(user, password)
else
@cell_phone.check_email(user, password)
end
end
end
In the above example Employee depends on cell_phone and laptop. When testing check_email you should have (at least) two tests, one for the case where @laptop.working? is true and one where it returns false. The second scenario (where @laptop.working? is false) is perfect for stub usage.
class EmployeeTest < Test::Unit::TestCase
class StubLaptop
def working?
false
end
end

...
def test_when_laptop_is_not_working_cell_phone_is_used
# code for setting up a mock for cell_phone
employee = Employee.new(mock_cell_phone, StubLaptop.new)
employee.check_email('jay', 'password')
# code to verify mock
end
end
The above test is correct; however, it can be slightly changed. Because of duck typing and the fact that stubs should always return a constant value it is possible to use class methods instead of instance methods. This relieves you from needing a new instance for each stub.
class EmployeeTest < Test::Unit::TestCase
class StubLaptop
def self.working?
false
end
end

...
def test_when_laptop_is_not_working_cell_phone_is_used
# code for setting up a mock for cell_phone
employee = Employee.new(mock_cell_phone, StubLaptop)
employee.check_email('jay', 'password')
# code to verify mock
end
end

Friday, May 26, 2006

Ruby Stunt Double

In the DSL focused system I'm currently working on we provide what I previously detailed as level 2 syntax checking. The DSL that we've developed is an internal DSL and requires the business user to call methods on objects (not that they have any idea what that means). The system depends on syntax similar to:
open.a.table.today.at 2
The users of the system requested some documentation to help them in building their business rules. Obviously we could have written or generated some documentation, but I thought displaying valid options within the system was a better choice.

The solution involves executing your DSL code in the syntax checking context and having key methods return Stunt Double instances. In the above code open is clearly a DSL keyword. In the syntax checking context open would be defined as:
def open
OpenStuntDouble.new(@messenger)
end
OpenStuntDouble is a class that inherits from StuntDouble and responds to all public instance methods of Open.
class OpenStuntDouble
def initialize(block)
super("open", &block)
end

stand_in_for(ThoughtWorks::Open) { AStuntDouble.new(@messenger) }
end
Any method that is executed on OpenStuntDouble will return a new instance of AStuntDouble. However, any method call that results in method_missing being executed will be reported as an invalid method call. Each invalid method call will also report the full list of available methods. Using this error you can provide your users with feedback on valid syntax.

Of course, the majority of the magic is in the StuntDouble class.
class StuntDouble

alias __methods__ methods
alias __class__ class

def initialize(name, &block)
raise "a messenger proc is required for stunt double" unless block_given?
@messenger = block
@name = name
end

def self.stand_in(*array)
block = block_given? ? Proc.new : Proc.new {}
array.each do |element|
define_method(element.to_sym, &block)
end
end

def self.stand_in_for(mod)
block = block_given? ? Proc.new : Proc.new {}
stand_in(*mod.public_instance_methods, &block)
end

def method_missing(sym, *args)
@notify_block.call "#{does_not_support(sym)} #{does_support}"
end

private

attr_reader :name

def does_support
return "Supported: #{valid_methods}." if valid_methods != ''
"Nothing is supported."
end

def does_not_support(sym)
"#{name} does not support #{sym.to_s}."
end

def valid_methods
result = self.__class__.public_instance_methods.sort.select do |operation|
!@@excluded.include? operation
end
result.join(', ')
end


@@excluded = public_instance_methods.select { |method| method =~ /^__.+/ }
@@excluded += %w(to_s inspect method_missing instance_eval)

instance_methods.each { |m| undef_method m unless @@excluded.include? m }

end
Of course, no code is complete without tests.
require File.expand_path(File.dirname(__FILE__) + "/../test_helper")

class StuntDoubleTest < Test::Unit::TestCase
class Inner < BlankSlate
def included_from_elsewhere
end
end

class FooStuntDouble < StuntDouble
end

class BarStuntDouble < StuntDouble
end

def setup_foo
FooStuntDouble.stand_in(:bar) { }
FooStuntDouble.stand_in_for(Inner)
end

def test_name_is_returned_from_message
setup_foo
stunt = FooStuntDouble.new("foo") do |error|
assert_equal 'foo does not support zeb. Supported: bar, included_from_elsewhere.', error
return
end
stunt.zeb
fail 'assertion skipped'
end

def test_name_is_returned_from_message_but_nothing_supported
stunt = BarStuntDouble.new("bar") do |error|
assert_equal 'bar does not support zeb. Nothing is supported.', error
return
end
stunt.zeb
fail 'assertion skipped'
end

def test_methods_are_added_from_array
setup_foo
stunt = FooStuntDouble.new("foo") do |error|
fail "error"
end
stunt.bar
end

def test_methods_are_added_from_class
setup_foo
stunt = FooStuntDouble.new("foo") do |error|
fail "error"
end
stunt.included_from_elsewhere
end

end
The end result is user friendly errors that can be displayed directly to your users.
open does not support the. Supported: a.
An advantage to this form of syntax help is that as the system evolves so will the help without any additional effort.

Thursday, May 25, 2006

Practical Ruby

My current project team started a blog for quick gotchas we discover while working on our project.

Pair Programming Anti-Patterns

Pair programming is hard. Previous to working at ThoughtWorks I had a manager who requested that the team do pair programming. The idea failed miserably, but it wasn't pair programming that was flawed, it was our approach. Below is a short list of the mistakes I've witnessed, but it's by no means a complete list.
  • People who don't care. This is the biggest pair programming killer. If your employees are only there for the paycheck they will make pair programming fail. Getting two employees on the same schedule is hard. Once two employees get on the same schedule one will daydream while the other works. This is very boring, but to people who don't care, it's better than working.
  • Strong code ownership. It's often very hard to motivate people to care about code they aren't responsible for. For more info on code ownership see Martin's bliki entry.
  • Inconsistent workstation setup. People like to work in their own environment. Unfortunately, when pairing you are unlikely to find two people who agree on what an environment should be. The solution is to provide pairing stations with standard setups. Instead of trying to learn multiple environments, the team can work on one shared setup.
  • Small work area. People like to be comfortable. Putting two chairs in one cube where one person's view is obstructed is very unlikely to produce good results. On the ThoughtWorks projects I've been staffed on we always have a project room that has enough room to sit side by side, two monitors that mirror the desktop, two mice, and two keyboards. This allows either person to take control when necessary.

Wednesday, May 24, 2006

Ruby extend and include

Module.include(module, ..) is commonly used to mix modules into classes and other modules. When a module is included the constants, methods, and module variables of the module are added to the including module or class instances.
module Actions
def left(steps)
Movement.new(self, steps)
end
end
The Actions module can be included to allow a class to generate movements.
class Car
include Actions
end
After Actions is included left can be executed by any instance of Car.
car = Car.new
movement = car.left

Similarly, Object.extend(module, ..) adds the instance methods from each module given as a parameter. However, extend adds the methods to one instance, not to all instances.
car = Car.new
car.extend Actions
movement = car.left
The above code gives car the same behavior as include would, except only the instance that called extend would have this new behavior. Therefore, the above code is valid, but this code would result in an error:
car = Car.new
car.extend Actions
car2 = Car.new
movement = car2.left #calling 'left' here is invalid because car2 does not extend Actions
extend is commonly used to mix instance methods of a module into a class.
module AttributeInitializer
def attr_init(name, klass)
eval "define_method(name) { @#{name} ||= klass.new }"
end
end

class Statement
extend AttributeInitializer

attr_init :charges, Array
end
Many DSL style class methods are added by extending a module instead of including it.

For another example of correct extend usage see Forwardable from the standard library.