From: pat eyler Date: 2006-11-22T08:13:49+09:00 Subject: Re: Ruby unit testing On 11/21/06, Jan Svitok wrote: > 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). Two ideas to think about here: * First, refactoring the code before you have tests is never a super good idea. Writing unit tests that you know you're going to need to refactor/replace/throw away is also. BDD and RSpec may be a better way to go (any BDD folks want to pitch in with some ideas here?). * Second, once you've got your suite of tests written (either unit tests or BDD style specifications), you should probably run your tests under rcov. C0 coverage (like rcov measures) won't ensure that you have all the tests you need, but it will help ensure that you're not leaving chunks of code untested. (Nice walk through below ... we can sure use more documents like this.) > > 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: > > 1. 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) > > 2. 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 > > 3. 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) > > 4. 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. > > 5. 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 > > 6. It seems that get_build_statuses is a more appropriate name for get_records. > rename it. > > 7. 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 > > 8. 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 > > 9. separate table extraction > make sure the tests are still working > add test for it > > 10. separate table parsing > make sure the tests are still working > add test for it > > 11. refactor more > make sure the tests are still working > > 12. 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 Gray 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(' 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 > ... > ... > > ... > 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 > > -- thanks, -pate ------------------------- http://on-ruby.blogspot.com