Late last year I attended some workshops which were being run as part of the YOW Melbourne developer conference. Since the workshops were run by @coreyhaines and @jbrains, TDD was a prominent part. Normally this would not be an issue, but in a spectacular display of fail (considering it was a developer conference in 2010), the internet was hard to come by, which left me and my freshly installed Linux laptop without the ability to acquire Rspec. Luckily a few weeks before, I decided to write a unit testing framework of my very own (just because I could :)) and so I had a reasonably fresh copy of that code lying around – problem solved. But, it got me thinking, what is the minimum amount of code needed to make a viable unit testing framework?

A Minimum Viable Unit Test

I had some minimal code when I first toyed with writing a unit testing framework, but then I went and ruined it by adding a bunch of features :), fortunately it is pretty easy to recreate. What we really want is the ability to execute the following code:

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 describe "some test" do it "should be true" do true . should == true end it "should show that an expression can be true" do ( 5 == 5 ) . should == true end it "should be failing deliberately" do 5 . should == 6 end end ``` As you can see it looks very much like a basic Rspec test, let's try and write some code to run this. ## Building A Basic Framework The first thing we're going to need is the ability to define a new test using "_describe_". Since we want to be able to put a "_describe_" block anywhere (_e.g. its own file_), we're going to have to augment Ruby a little bit. **The "_puts_" method lives in the Kernel module and is therefore available anywhere (_since the Object class includes Kernel and every object in Ruby inherits from Object_), we will put describe inside Kernel to give it the same ability**: ``` ruby module Kernel def describe ( description , & block ) tests = Dsl . new . parse ( description , block ) tests . execute end end

As you can see, “describe” takes a description string and a block that will contain the rest of our test code. At this point, we want to pull apart the block that we’re passing to “describe” to separate it into individual examples (i.e. “it” blocks). For this we create a class called Dsl and pass our block to its parse method, this will produce an object that will allow us to execute all our test, but let’s not get ahead of ourselves. Our Dsl class looks like this:

1 2 3 4 5 6 7 8 9 10 11 12 class Dsl def initialize @tests = {} end def parse ( description , block ) self . instance_eval ( & block ) Executor . new ( description , @tests ) end def it ( description , & block ) @tests [ description ] = block end end

What we do here is evaluate our block in the context of the current Dsl object:

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 self . instance_eval ( & block ) ``` Our **Dsl** object has an "_it_" method which also takes a description and a block and since that is exactly what our describe block contains everything works well (_i.e. we're essentially making several method calls to the "it" method each time passing in a description and a block_). **We could define other methods on our Dsl object and those would become part of the "_language_" which will be available to us in the "_describe_" block**. Our "_it_" method will be called once for every "_it_" block in the describe block, every time that happens we simply take the block that was passed in and store it in hash keyed on the description. When we're done, we simply create a new **Executor** object which we will use to iterate over our test blocks, call them and produce some results. The executor looks like this: ``` ruby class Executor def initialize ( description , tests ) @description = description @tests = tests @success_count = 0 @failure_count = 0 end def execute puts " #{ @description } " @tests . each_pair do | name , block | print " - #{ name } " result = self . instance_eval ( & block ) result ? @success_count += 1 : @failure_count += 1 puts result ? " SUCCESS" : " FAILURE" end summary end def summary puts "

#{ @tests . keys . size } tests, #{ @success_count } success, #{ @failure_count } failure" end end ``` Our executor code is reasonably simple. We print out the description of our "_describe_" block we then go through all the "_it_" blocks we have stored and evaluate them in the context of the executor object. In our case there is no special reason for this, but it does mean that the executor object can also be a container for some methods that can be used as a "_language_" inside "_it_" blocks (_i.e. part of our dsl can be defined as method on the executor_). For example, we could define the following method on our executor: ``` ruby def should_be_five ( x ) 5 == x end ``` This method would then be available for us to use inside our "_it_" blocks, but for our simple test it is not necessary. So, we evaluate our "_it_" blocks and store the result, which is simply going to be the return value of the last statement in the "_it_" block (_as per regular Ruby_). **In our case we want to make sure that the last statement always returns a boolean value (_to indicate success or failure of the test_), we can then use it to produce some meaningful output**. We are missing one piece though, the "_should_" method, we had code like: ``` ruby true . should == true 5 . should == 5 ``` It seems that every object has the "_should_" method available to it and that's exactly how it works: ``` ruby class Object def should self end end

The method doesn’t really DO anything (just returns the object); it simply acts as syntactic sugar to make our tests read a bit better.

At this stage, we just take the result of evaluating the test, turn it into a string to indicate success or failure and print that out. Along the way we keep track of the number of successes or failures so that we can produce a summary report in the end. That’s all the code we need, if we put it all together, we get the following 44 lines:

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 module Kernel def describe ( description , & block ) tests = Dsl . new . parse ( description , block ) tests . execute end end class Object def should self end end class Dsl def initialize @tests = {} end def parse ( description , block ) self . instance_eval ( & block ) Executor . new ( description , @tests ) end def it ( description , & block ) @tests [ description ] = block end end class Executor def initialize ( description , tests ) @description = description @tests = tests @success_count = 0 @failure_count = 0 end def execute puts " #{ @description } " @tests . each_pair do | name , block | print " - #{ name } " result = self . instance_eval ( & block ) result ? @success_count += 1 : @failure_count += 1 puts result ? " SUCCESS" : " FAILURE" end summary end def summary puts "

#{ @tests . keys . size } tests, #{ @success_count } success, #{ @failure_count } failure" end end

If we “require” this code and execute our original sample test, we get the following output:

some test - should be true SUCCESS - should show that an expression can be true SUCCESS - should be failing deliberately FAILURE 3 tests, 2 success, 1 failure

Nifty! Now, if you’re ever stuck without a unit testing framework and you don’t want to go cowboy, just spend 5 minutes and you can whip up something reasonably viable to tide you over. I am, of course, exaggerating slightly; you will quickly start to miss all the extra expectations, better output, mocking and stubbing etc. However we can easily enhance our little framework with some of those features (e.g. adding extra DSL elements) – the development effort is surprisingly small. If you don’t believe me, have a look at bacon it is only a couple of hundred lines and is a reasonably complete little Rspec clone. Attest – the framework that I wrote is another decent example (even if I do say so myself :P). Both of them do still miss any sort of built-in test double support, but adding that is a story for another time.

Image by jepoirrier