Hi,
This is how I’d do it. I don’t know the details, so your actual code
will be different. This code was not run at all, so probably there are
many typos/errors.
You’d certainly choose another names than I’ve chosen.
First of all, if you want to test, it’s good to split the code in
small chunks so that each does a little bit of work. Thus you can test
them separately. I tend to put the code inside some class as well…
you know, the OOP
While adding tests, you want to change the code as little as possible,
to not broke it. When you have the tests in place, you can safely
change stuff (=refactor).
The final code could be a bit different from what is written in the
mail, as the code was written before the text, and I was too lazy to
fix it
Now the steps to the code below:
- minor corrections:
- at the end of the code ‘end’ is missing
- change |records| to |result| as you are iterating over Result(s)
- change each to collect as you seem to collect results (but I may be
wrong)
- move the whole each/collect block inside Result class as:
class Result < …
def self.get_records
Result.find(:all).each do |result|
…
end
end
end
(you can omit the Result. before find)
We have a function that returns something for all Results
what remains at the bottom is
records = Result.get_records
- The block doesn’t return anything meaningful so:
- add just after class Result < …:
BuildStatus = Struct.new(:module_name, :build_status_since,
:last_failure, :last_success, :build_number)
- add BuildStatus.new(module_name, build_status_since, last_failure,
last_success, build_number) at the end of the block (inside it)
- Now we can add the first unit test at the very bottom:
comment out the records = … line
and add:
if FILE == $0
require ‘test/unit’
class TestResult < Test::Unit::TestCase
def test_get_records
assert_equal [], Result.get_records
end
end
end
This test will fail if there are any results, but we have SOMETHING now.
-
Let’s make the test succeed:
change the inside of the test:
def test_get_records
results = Result.find(:all)
records =Result.get_records
assert_equal results.size, records.size
records.each do |record|
assert_equal Result::BuildStatus, record.class
end
end
-
It seems that get_build_statuses is a more appropriate name for
get_records.
rename it.
-
Now let’s go through the block content:
move it to separate method:
def get_build_status
…
end
and change Result.get_build_statuses to:
def self
find(:all).collect do |result|
result.get_build_status
end
end
make sure the tests are still working
add test for get_build_status (it should return a BuildStatus),
check it’s contents
i.e.
def test_get_build_status
result =Result.new
result.build_url = ‘whatevr yo use’
build_status = result.get_build_status
assert_equal ‘…’, build_status.module_name
…
end
-
move fetch inside the class, adding default parameter build_url, so
we can call
result.fetch insteadof fetch(result.build_url)
add test for fetch, make sure the tests are still working
-
separate table extraction
make sure the tests are still working
add test for it
-
separate table parsing
make sure the tests are still working
add test for it
-
refactor more
make sure the tests are still working
-
when you’re done, you can drop some tests if they are no more
needed.
Finally one really nice link:
http://macromates.com/screencast/ruby_quiz_screencast.mov
This is James Edward G. doing RubyQuiz along with testing.
----------8<-------------------------------------
require ‘net/http’
require ‘uri’
require ‘rexml/document’
require ‘rubygems’
require_gem ‘activerecord’
include REXML
#Connect to the database
ActiveRecord::Base.establish_connection(
:adapter => “mysql”,
:username => “root”,
:host => “localhost”,
:password => “”,
:database => “build”
)
puts “connected to build database”
class Result < ActiveRecord::Base
# default value for uri_str to allow calling result.fetch
# instead of fetch(result.build_url)
def fetch(uri_str = build_url, limit=10)
fail ‘http redirect too deep’ if limit.zero?
puts “Scraping: #{uri_str}”
response = Net::HTTP.get_response(URI.parse(uri_str))
case response
when Net::HTTPSuccess
response
when NetHTTPRedirection
fetch(response[‘location’], limit-1)
else
response.error!
end
end
def get_table(data)
table_start_pos = data.index('<table class="index"
width=“100%”>‘)
table_end_pos = data.index(’') + 9
height = table_end_pos - table_start_pos
#pick out the table
# response.body = data
# you can do without heigth:
# return data[table_start_pos...table_stop_pos]
return data[table_start_pos,height]
end
BuildStatus = Struct.new(:module_name, :build_status_since,
:last_failure, :last_success, :build_number)
def parse_table(table)
ret = BuildStatus.new
#convert Data to REXML
# - added parentheses
converted_data = REXML::Document.new(table)
module_name = XPath.first(converted_data,
“//td[@class=‘data’]/a/]”)
ret.module_name = module_name
build_status_since = XPath.first(converted_data,
“//td[2]/em”)
build_status_since = build_status_since.text
ret.build_status_since =
build_status_since.slice(/(\d+):(\d+)/)
last_failure = XPath.first(converted_data,
“//tbody/tr/td[3]”)
ret.last_failure = last_failure.text
last_success = XPath.first(converted_data,
“//tbody/tr/td[4]”)
ret.last_success = last_success.text
build_number = XPath.first(converted_data,
“//tbody/tr/td[5]”)
ret.build_number = build_number.text
ret
end
def get_build_status
# - move fetch inside Result class
response = fetch
# you can put .body into fetch, and combine the
following two into one:
# table = get_table(fetch())
# or even in one line:
# return parse_table(get_table(fetch()))
scraped_data = response.body
table = get_table(scraped_data)
build_status = parse_table(table)
return build_status
end
def self.get_build_statuses
find(:all).collect do |result|
result.get_build_status
end
end
end
if FILE == $0
require ‘test/unit’
class TestResult < Test::Unit::TestCase
EXAMPLE_PAGE = <<-EOF
…
EOF
EXAMPLE_TABLE = <<-EOF
…<table…>
…
...
EOF
EXAMPLE_RESULT = Result::BuildStatus.new('','','','')
fill in the blanks
def setup
@r = Result.new
end
def test_fetch
@r.build_url = 'whatever' # fill in your example
assert_raise(RuntimeError) { @r.fetch('', 0) }
zero is ‘too deep’
assert_equal @r.fetch(@r.build_url), @r.fetch
check for refactoring
end
def test_get_table
assert_equal EXAMPLE_TABLE,
@r.get_table(EXAMPLE_PAGE)
end
def test_parse_table
assert_equal EXAMPLE_RESULT,
@r.parse_table(EXAMPLE_TABLE)
end
def test_get_result
assert_equal Result::BuildStatus.new('',...),
r.get_build_status
end
end
end