My main website   : :  My programming articles

To see thumbnails of all slides, hit Esc

Stubs in unit tests

using Mocha, a Ruby mock/stub framework

Created by Elze Hamilton / @elze

A lightning talk for Austin All Girl Hack Night

What are stubs?

Problem with unit tests:

Our code often interacts with the database.

In the course of development, the state of the database frequently changes: records are added and deleted.

But our unit tests depend on having certain data available.

Example of a method under test

This is a (very contrived) application that collects website visitor statistics. Suppose that any time a visitor visits our website, the record of their visit is written to the simple_visits table in the database.

The application is just one class, StatisticsCollector, with one method, CollectStatistics. This method:

  • gets all the SimpleVisit records where operating system is iOS
    (SimpleVisit.where(:os => "iOS")),
  • from the iOS_visits array gets all the unique screen resolution values,
  • from the resolution_array gets all the screen width values,
  • finds and returns minimum screen width.

StatisticsCollector class with a method CollectStatistics


		
require './SimpleVisit'
class StatisticsCollector
  def CollectStatistics

    iOS_visits = SimpleVisit.where(:os => "iOS")
    resolution_array = iOS_visits.map{ |visit| visit.resolution }.uniq

    min_screen_width_array = []

    resolution_array.each do |res|
      (width, height) = res.split("x")
      min_screen_width_array.push(width)
    end
    
    min_screen_width_array.sort
    puts "Minimum screen width = " + min_screen_width_array[0].to_s()
    min_screen_width_array[0].to_i()
  end
end
	    

Unit tests should be independent of

  • database
  • and any external resources, such as files or network.

How should we supply database records to our unit tests, then?

That's where stubs come into play.

A stub is an object that "pretends" to act like a real object.

You can give a stub a few methods that would return just the values you need, and nothing else.

Our stubs will simulate database records.

To be completely accurate, our stubs will simulate some methods in a class SimpleVisit, which is derived from ActiveRecord. (ActiveRecord objects are objects that wrap around database records.)

Many programming languages have frameworks for creating stubs.

Ruby makes especially easy to create them with the Mocha framework.

The following example will show how to add stub methods to SimpleVisit class.

First, here is the SimpleVisit class itself.


		
require 'active_record'
		
ActiveRecord::Base.establish_connection(
"postgres://username:password@localhost/postgres"
)

class SimpleVisit < ActiveRecord::Base
end
	      

We need to stub these SimpleVisit methods:

where

(as in SimpleVisit.where(:os => "iOS")),

and

resolution

which is the attribute accessor in

iOS_visits.map{ |visit| visit.resolution }.uniq

where is a class method (analogous to static methods in Java, C#, or C++).

resolution is an instance method.

We will stub these methods in the unit test.

This is the unit test


class StatisticsCollectorTest < Test::Unit::TestCase  
  def test_count_ios_unique_screen_width 		
    visit_1 = SimpleVisit.new # We'll pretend it has :os = "iOS"
    visit_1.stubs(:resolution).returns("320x568")
  
    visit_2 = SimpleVisit.new # We'll pretend it has :os = "iOS"
    visit_2.stubs(:resolution).returns("320x568")
  
    visit_3 = SimpleVisit.new # We'll pretend it has :os = "Win7"
    visit_3.stubs(:resolution).returns("1440x900")
  
    visit_4 = SimpleVisit.new # We'll pretend it has :os = "iOS"
    visit_4.stubs(:resolution).returns("768x1024") 

    statisticsCollector = StatisticsCollector.new
  
    # The stubbed "where" method returns just the visits pretending to be from iOS.
    SimpleVisit.stubs(:where).returns([visit_1, visit_2, visit_4])
    assert_equal 320, statisticsCollector.CollectStatistics
  end
end

	    

What's happening here?

visit_1 = SimpleVisit.new

creates an "empty" SimpleVisit object. All its fields are uninitialized.

visit_1.stubs(:resolution).returns("320x568")

creates a stub for resolution method.

Normally, this method would access SimpleVisit data member called resolution.

Stubbing it essentially means "when somebody asks you for this visit's resolution, return "320x568"."

Note that we do NOT have to stub the os method. That's because CollectStatistics does not call it.

But you might ask: what about this line in CollectStatistics:

iOS_visits = SimpleVisit.where(:os => "iOS")

Doesn't where check if a SimpleVisit's os field is set to iOS?

The answer is:

The stubbed where method does not check what os field is set to.

You might remember that it is

SimpleVisit.stubs(:where).returns([visit_1, visit_2, visit_4])

So it returns the SimpleVisit objects of your choice without caring about their content.

Unit test passes

Unit test passes

I ran the unit test. It showed "0 failures, 0 errors", so it passed.

The beauty of this:

statisticsCollector.CollectStatistics method does not even know that we are passing fake, or "hollow" SimpleVisit objects to it from the unit test. It treats them as real things, because we have stubbed all the methods it calls: resolution and where.

But when statisticsCollector.CollectStatistics gets called by the actual program, SimpleVisit objects will have the real database data in them, because the real where method will populate them.

Thus we don't need to change the code of the class under test: it will act the same whether it's receiving database-backed objects, or stub objects. It doesn't know the difference.

To summarize:

Don't let your unit tests interact with the database: database records are always changing, and your tests will fail.

Instead,

  • Create a bunch of "empty" database objects in your test;

  • Stub their methods that are being used in the function under test.

The testing framework that provides the stub capability is called Mocha.

Ruby has other stub/mock frameworks, but I found Mocha the most intuitive.