I see unit testing as a way to test each possible snippet of functionality and route the code in question can take. With Ruby being such a dynamic language and allowing shortcuts to common problems, sometimes it can seem somewhat of a mystery, how to test these snippets of functionality.
Using Memoization as an example:
class MyClass def lazy_initialized_value @lazy_initialized_value ||= Expensive.request end end
There are actually 3 separate snippets of functionality that need testing here, however it is not immediately obvious from the example. Lets be slightly more verbose about what is actually happening:
class MyClass def lazy_initialized_value @lazy_initialized_value = Expensive.request unless @lazy_initialized_value @lazy_initialized_value end end
Now it is much easier to see the 3 steps the code should take:
* Store result of expensive request in instance variable
* Leave instance variable alone when it is already set
* Return the value of the instance variable
Now we have this information, our tests become (using Mocha to mock external methods):
class Expensive; end module Tests::MyClass # lazy_initialized_value # ---------------------- class LazyInitializedValueTest < Test::Unit::TestCase def test_should_respond assert_respond_to MyClass.new, :lazy_initialized_value end def test_should_store_result_of_expensive_request_in_instance_variable instance = MyClass.new Expensive.stubs(:request).with().returns('expensive value') instance.lazy_initialized_value assert_equal 'expensive value', instance.instance_variable_get('@lazy_initialized_value') end def test_should_return_value_of_instance_varable instance = MyClass.new instance.instance_variable_set '@lazy_initialized_value', 'the value' Expensive.stubs(:request) assert_equal 'the value', instance.lazy_initialized_value end def test_should_maintain_existing_instance_variable_value_when_already_set instance = MyClass.new instance.instance_variable_set '@lazy_initialized_value', 'existing value' Expensive.stubs(:request) instance.lazy_initialized_value assert_equal 'existing value', instance.instance_variable_get('@lazy_initialized_value') end end end
Now we have these tests in place, we can go back and refractor the code ’til our heart’s content using all the tricks in the book but by simplifying the problem in the first place, it gives us a solid test suite and the confidence to make changes without breaking functionality.
If you were solving this problem test-first then you wouldn’t (but more likely, shouldn’t) have written the first example until re-factoring stage anyway, however when these shortcuts become engrained in your brain, it’s all too easy to forget what they are _actually_ doing.
So there we go, simplify the initial implementation, get a solid test suite in order, _then_ re-factor.