Update of /cvsroot/mockobjects/no-stone-unturned/src/ruby In directory usw-pr-cvs1:/tmp/cvs-serv23153 Modified Files: addrbook.rb addrbook_test.rb addresses.txt addrservlet.rb addrservlet_test.rb notes.txt tasks.txt Added Files: addrbook_mock.rb Log Message: Story 2 complete --- NEW FILE: addrbook_mock.rb --- require 'mockobjects' class MockAddressBook < MockObject def initialize( entries = {} ) @entries = entries @each_entries = nil end def empty? @entries.empty? end def _setup_index( name, address ) @entries[name] = address end def has_key?( name ) @entries.has_key?(name) end def []( name ) @entries[name] end def _setup_each( *entries ) @each_entries = entries end def each if @each_entries @each_entries.each else @entries.each end end end Index: addrbook.rb =================================================================== RCS file: /cvsroot/mockobjects/no-stone-unturned/src/ruby/addrbook.rb,v retrieving revision 1.1 retrieving revision 1.2 diff -u -r1.1 -r1.2 --- addrbook.rb 2 Sep 2002 14:53:00 -0000 1.1 +++ addrbook.rb 2 Sep 2002 14:56:58 -0000 1.2 @@ -3,16 +3,23 @@ class AddressBook def initialize( filename, file_store=File ) @entries = Hash.new - read_file( file_store, filename ) end + def empty? + @entries.empty? + end + def has_key?( name ) @entries.has_key?(name) end def []( name ) @entries[name] + end + + def each + @entries.each end def read_file( file_store, filename ) Index: addrbook_test.rb =================================================================== RCS file: /cvsroot/mockobjects/no-stone-unturned/src/ruby/addrbook_test.rb,v retrieving revision 1.1 retrieving revision 1.2 diff -u -r1.1 -r1.2 --- addrbook_test.rb 2 Sep 2002 14:53:00 -0000 1.1 +++ addrbook_test.rb 2 Sep 2002 14:56:58 -0000 1.2 @@ -9,7 +9,7 @@ @name = Expectation.new("name") @mode = Expectation.new("mode") @open_proc = Expectation.new("open_proc") - @open_result = nil + @open_action = proc { nil } end def _expect_name( &checks ) @@ -25,8 +25,13 @@ @open_proc.expect( &checks ) end - def _setup_open( result ) - @open_result = result + def _setup_open( result=nil, &proc ) + if proc + raise "result should not be given if block given" if result + @open_action = proc + else + @open_action = proc { result } + end end def open( name, mode, &proc ) @@ -34,11 +39,13 @@ @mode.actual = mode @open_proc.actual = proc + result = @open_action.call( name, mode ) + if proc - proc.call( @open_result ) - @open_result.close + proc.call( result ) + result.close else - return @open_result + return result end end end @@ -81,10 +88,51 @@ book = AddressBook.new(FILENAME,store) + assert( !book.empty?, "not empty" ) assert( book.has_key?("NAME1"), "has key NAME1" ) assert_equal( "ADDRESS1", book["NAME1"] ) assert( book.has_key?("NAME2"), "has key NAME2" ) assert_equal( "ADDRESS2", book["NAME2"] ) + + contents._verify + store._verify + end + + def test_empty_file + contents = MockInputStream.new + + store = MockFileStore.new + store._expect_name { |name| assert_equal( FILENAME, name ) } + store._expect_mode { |mode| assert_match( /r/, mode, "read mode" ) } + store._expect_open_proc { |proc| assert_not_nil(proc,"open proc") } + store._setup_open( contents ) + + book = AddressBook.new( FILENAME, store ) + + assert( book.empty?, "is empty" ) + + contents._verify + store._verify + end + + def test_each + contents = MockInputStream.new( "NAME1=ADDRESS1", "NAME2=ADDRESS2" ) + + store = MockFileStore.new + store._expect_name { |name| assert_equal( FILENAME, name ) } + store._expect_mode { |mode| assert_match( /r/, mode, "read mode" ) } + store._expect_open_proc { |proc| assert_not_nil(proc,"open proc") } + store._setup_open( contents ) + + book = AddressBook.new(FILENAME,store) + + names = {} + book.each { |name,address| names[name] = address } + + assert( names.has_key?("NAME1") ) + assert_equal( "ADDRESS1", names["NAME1"] ) + assert( names.has_key?("NAME2") ) + assert_equal( "ADDRESS2", names["NAME2"] ) contents._verify store._verify Index: addresses.txt =================================================================== RCS file: /cvsroot/mockobjects/no-stone-unturned/src/ruby/addresses.txt,v retrieving revision 1.1 retrieving revision 1.2 diff -u -r1.1 -r1.2 --- addresses.txt 2 Sep 2002 14:53:00 -0000 1.1 +++ addresses.txt 2 Sep 2002 14:56:58 -0000 1.2 @@ -1,3 +1,3 @@ -Nat Pryce=nat...@so..., +Nat Pryce=nat...@so... Steve Freeman=st...@so... Jeff Martin=cus...@so... Index: addrservlet.rb =================================================================== RCS file: /cvsroot/mockobjects/no-stone-unturned/src/ruby/addrservlet.rb,v retrieving revision 1.2 retrieving revision 1.3 diff -u -r1.2 -r1.3 --- addrservlet.rb 2 Sep 2002 14:53:00 -0000 1.2 +++ addrservlet.rb 2 Sep 2002 14:56:58 -0000 1.3 @@ -9,6 +9,64 @@ end def do_GET( request, response ) + command = request.path_info + + case command + when "/list" + do_list( request, response ) + when "/search" + do_search( request, response ) + else + raise WEBrick::HTTPStatus::NotFound + end + end + + def do_list( request, response ) + if @address_book.empty? + respond_empty_book( response ) + else + list_addresses( response ) + end + end + + def respond_empty_book( response ) + response['content-type'] = 'text/plain' + response.body = "no addresses" + end + + def list_addresses( response ) + response['content-type'] = 'text/html' + + body = <<-EOF + <html><head><title>Address Book</title></head> + <body><table> + EOF + + entries = [] + @address_book.each do |name,address| + entries << [name,address] + end + + entries.sort! do |e,f| + e_surname = e[0].split[-1] + f_surname = f[0].split[-1] + e_surname <=> f_surname + end + + entries.each do |e| + body << <<-EOF + <tr><td>#{e[0]}</td><td>#{e[1]}</td></tr> + EOF + end + + body << <<-EOF + </table></body></html> + EOF + + response.body = body + end + + def do_search( request, response ) response['content-type'] = 'text/plain' query = request.query_string Index: addrservlet_test.rb =================================================================== RCS file: /cvsroot/mockobjects/no-stone-unturned/src/ruby/addrservlet_test.rb,v retrieving revision 1.1 retrieving revision 1.2 diff -u -r1.1 -r1.2 --- addrservlet_test.rb 2 Sep 2002 14:53:00 -0000 1.1 +++ addrservlet_test.rb 2 Sep 2002 14:56:58 -0000 1.2 @@ -4,13 +4,24 @@ require 'webrick' require 'mockobjects' require 'addrservlet' +require 'addrbook_mock' class MockRequest < MockObject + def initialize + @query_string = "" + @path_info = "" + end + def _setup_query_string( query ) @query_string = WEBrick::HTTPUtils::escape_form(query) end attr_reader :query_string + + def _setup_path_info( path_info ) + @path_info = path_info + end + attr_reader :path_info end @@ -44,39 +55,30 @@ end -class MockAddressBook - def initialize( entries ) - @entries = entries - end - - def has_key?( name ) - @entries.has_key?(name) - end - - def []( name ) - @entries[name] - end -end - - class AddressBookServletTest < Test::Unit::TestCase - NAME1 = "First Last" - ADDR1 = "ADDRESS" + NAME1 = "Pete Brown" # Note: in reverse alphabetical order + ADDR1 = "Address1" + NAME2 = "John Adams" + ADDR2 = "Address2" + def set_up @request = MockRequest.new @response = MockResponse.new - @book = MockAddressBook.new( NAME1 => ADDR1 ) + @book = MockAddressBook.new( NAME1 => ADDR1, NAME2 => ADDR2 ) @servlet = AddressBookServlet.new( {}, @book ) end def test_no_address_found + @request._setup_path_info("/search") @request._setup_query_string( "UNKNOWN NAME" ) - @response._expect_header("content-type") do |value| - assert_match( /^text\/.*/, value ) + @response._expect_header("content-type") do |type| + assert_match( /^text\/.*/, type ) + end + @response._expect_body do |text| + assert_match( /no address found/, text ) end - @response._expect_body { |text| assert_match( /no address found/, text ) } @servlet.do_GET( @request, @response ) @@ -84,10 +86,13 @@ end def test_no_address_found_when_no_name + @request._setup_path_info("/search") @response._expect_header("content-type") do |value| assert_match( /^text\/.*/, value ) end - @response._expect_body { |text| assert_match( /no address found/, text ) } + @response._expect_body do |text| + assert_match( /no address found/, text ) + end @servlet.do_GET( @request, @response ) @@ -95,15 +100,83 @@ end def test_address_found + @request._setup_path_info("/search") @request._setup_query_string( NAME1 ) - @response._expect_header("content-type") do |value| - assert_match( /^text\/.*/, value ) + @response._expect_header("content-type") do |type| + assert_match( /^text\/.*/, type ) end @response._expect_body { |text| assert_match( /#{ADDR1}/, text ) } @servlet.do_GET( @request, @response ) @response._verify + end + + def test_list_all_empty_book + book = MockAddressBook.new( {} ) + servlet = AddressBookServlet.new( {}, book ) + + @request._setup_path_info("/list") + + @response._expect_header("content-type") do |type| + assert_match( /^text\/.*/, type ) + end + @response._expect_body { |text| assert_match( /no addresses/, text ) } + + servlet.do_GET( @request, @response ) + + @response._verify + end + + def test_list_entries + @request._setup_path_info("/list") + + @response._expect_header("content-type") do |type| + assert_equal( "text/html", type ) + end + @response._expect_body do |text| + assert_address_as_table_row( text, NAME1, ADDR1 ) + assert_address_as_table_row( text, NAME2, ADDR2 ) + end + + @servlet.do_GET( @request, @response ) + + @response._verify + end + + def test_list_is_sorted + @request._setup_path_info("/list") + + @book._setup_each( [NAME1,ADDR1],[NAME2,ADDR2] ) # reverse order + + @response._expect_header("content-type") do |type| + assert_equal( "text/html", type ) + end + @response._expect_body do |text| + name1_index = text.index(NAME1) + name2_index = text.index(NAME2) + assert_not_nil( name1_index, "#{NAME1} in body" ) + assert_not_nil( name2_index, "#{NAME2} in body" ) + assert( name1_index > name2_index, "names in alphatical order" ) + end + + @servlet.do_GET( @request, @response ) + + @response._verify + end + + def test_invalid_command + @request._setup_path_info("*INVALID*") + + assert_raises WEBrick::HTTPStatus::NotFound do + @servlet.do_GET( @request, @response ) + end + + @response._verify + end + + def assert_address_as_table_row( body, name, address ) + assert_match( /<tr><td>#{name}<\/td><td>#{address}<\/td><\/tr>/, body ) end end Index: notes.txt =================================================================== RCS file: /cvsroot/mockobjects/no-stone-unturned/src/ruby/notes.txt,v retrieving revision 1.1 retrieving revision 1.2 diff -u -r1.1 -r1.2 --- notes.txt 2 Sep 2002 14:53:00 -0000 1.1 +++ notes.txt 2 Sep 2002 14:56:58 -0000 1.2 @@ -9,6 +9,8 @@ For example, it is trivial to mock the file system in Ruby, but it is quite hard to do so in Java (see the alt.java.* packages in the Mock Objects library). + + STORY 1, TASK a 2) Expectations can be defined using closures, rather than various expectation classes. Closures can call the Test::Unit assertions @@ -17,6 +19,8 @@ This makes mock-object classes very flexible, because expectations can be tailored for each test method. + STORY 1, TASK b + 3) There is less need to define factories to insert mock objects into your classes. In Ruby, classes just objects with a "new" method, and sometimes other methods that create objects. That is, classes @@ -29,6 +33,8 @@ the stub responds to the same messages as a class, and so to all intents and purposes *is* a class. + STORY 1, TASK c + 4) NOTE: POTENTIAL PITFALL Servlet used a hash to store names. Changed to use an address book @@ -55,3 +61,17 @@ sync, so that they both implement the same methods. That way you catch errors where client code is being tested against a mock that does not actually implement the same methods as the real class that it is mocking. + + STORY 1, TASK c + +5) Organising tests and mocks. + <file>.rb + <file>_test.rb + <file>_mock.rb + + STORY 1, TASK c + + + Put this in a sidebar + + Index: tasks.txt =================================================================== RCS file: /cvsroot/mockobjects/no-stone-unturned/src/ruby/tasks.txt,v retrieving revision 1.2 retrieving revision 1.3 diff -u -r1.2 -r1.3 --- tasks.txt 2 Sep 2002 14:53:00 -0000 1.2 +++ tasks.txt 2 Sep 2002 14:56:58 -0000 1.3 @@ -1,29 +1,30 @@ -Stories -1) anyone can search the entries in the book. -2) anyone can add their name and email address to the book. -3) anyone can remove an entry from the book. +Story: Load from file, do search -Tasks +Note: don't need to check for errors, because errors when loading file will + stop server at startup. -1a) accept a name and find no result -DONE +Story: List all the entries in the book -Points of Interest: - - no need to implement any interfaces when defining mocks +Tasks: + * Branch on servlet path: search vs. list + DONE + + * Show an empty book. + DONE + * Show all the entries + DONE -1b) accept a name and return a result from a hard-coded collection. -1b.1) what about the servlet receiving no name? -DONE + * Sort the results alphabetically + DONE. -Points of Interest: - - using blocks to handle expectations +Note: need to add _setup_each to MockAddressBook to force a particular + interation order because Hash doesn't specify order + * Invalid path? + DONE -REFACTORING - - refactor code - - refactor tests to use expectation and mockobject classes +Note: throws HTTPStatus::NotFound exception when unknown path info +Note: requires changing tests to explicitly set path_info to "/search" where + no explicit path info eas specified - -1c) Retrieve the entries from a file, specified as a servlet property. - Values are held in memory. |