My main website : : My programming articles
To see thumbnails of all slides, hit Esc
Created by Elze Hamilton / @elze
A lightning talk for Austin All Girl Hack Night
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.
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:
SimpleVisit
records where operating system is iOS SimpleVisit.where(:os => "iOS")
),iOS_visits
array gets all the unique screen resolution values,resolution_array
gets all the screen width values,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
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
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.
I ran the unit test. It showed "0 failures, 0 errors", so it passed.
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.
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.