Tuesday, March 4, 2008

Performance issue in Dictionary of enum types

Interesting performance issue: if you have a System.Collections.Generic.Dictionary with key type being an Enum type each request to it performs a boxing of the key due to specifics of JIT optimizations. If you use such constructs in performance-critical parts of your code be sure to replace the default IEqualityComparer with a custom one.

Coroutines and Mocking

A year ago I played with Ruby and in the course of investigating its possibilities I created a simple yet interesting mocking framework. I don't use mocking frameworks a lot in my testing code, but one thing that hurts me each time I see samples of mock objects in use is that strange notations for describing the expected behavior.

It usually goes like this (this example uses FlexMock, a Ruby mock objects framework):
  
mock.should_receive(:average).with(12).once

mock.should_receive(:average).with(Integer).
at_least.twice.at_most.times(10).
and_return { rand }

You can see that one must use special methods (which in this cases resemble plain English, but nonetheless) to describe what is expected to happen with mock object in the course of text execution. The problem I see is that it's not easy to quickly understand what's going on because the expectation code doesn't really look like normal code that should be written in order to meet these expectations.

What I always wanted to have as part of my imaginary mocking framework is the ability to simply write down the essence of what I want to happen:


amount = 100
atm.transfer(from, to, amount) do
from.withdraw(amount)
to.deposit(amount)
end


Here I invoke a transfer method on ATM object and expect it to first withdraw the specified amount from account from and then deposit the same amount to account to.

So I tried to do that and found a very simple way to implement this kind of framework using coroutines in Ruby. So with the simple skeleton I managed to write you can actually write the code like this:

require 'mock.rb'

class ATM
def transfer( from, to, amount )
t = from.withdraw(amount)
to.deposit(amount, amount)
end
end

from = MockObject.new
to = MockObject.new
atm = MockHost.new(ATM.new)

amount = 100
atm.transfer(from, to, amount) do
from.withdraw(amount) { "Transaction cookie" }
to.deposit(amount, "Transaction cookie")
end

This sample test will fail because of a (silly) bug in transfer method that passes invalid value as transaction identifier to deposit method.

You may think that this requires a huge and complex backend, but it doesn't: the skeleton for MockObject/MockHost is only about 80 lines. Of course it lacks many features that fully-fledged mocking frameworks have but I think it can be easily extended.


class MockHost
class << self
def block
@@block
end

def block=(block)
@@block = block
end
end

def initialize( realObject )
@realObject = realObject
@executing = false
end

def method_missing(method_name, *args, &block)
was_executing = @executing
begin
m = @realObject.method(method_name)
raise NoMethodError if not m
@@block = block
if was_executing
m.call(*args, &block)
else
@executing = true
m.call(*args)
end
ensure
@executing = false if not was_executing
end
end
end

class MockObject
def method_missing(method_name, *args, &block)
if @method_name
call_from_test(method_name, *args, &block)
else
call_from_main_block(method_name, *args)
end
end

def call_from_test(method_name, *args, &block)
p "Method #{method_name} does not match expected method #{@method_name}" if method_name != @method_name
p "Arguments count #{args.length} does not match expected count #{@args.length}" if args.length != @args.length

0.upto(args.length - 1) do |i|
real_arg = @args[i]
expected_arg = args[i]

p "Argument #{i} doesn't match. Expected #{expected_arg} but was #{real_arg}" if real_arg != expected_arg
end

@retvalue = block.call if block

callcc do |continue_test_block|
MockHost.block = continue_test_block
@continue_main_block.call
end
end

def call_from_main_block(method_name, *args)
@method_name = method_name
@args = args
begin
callcc do |@continue_main_block|
MockHost.block.call if MockHost.block
end

@retvalue
ensure
@method_name = nil
@args = nil
end
end
end