From: Nat P. <np...@us...> - 2002-09-02 14:53:05
|
Update of /cvsroot/mockobjects/no-stone-unturned/src/ruby In directory usw-pr-cvs1:/tmp/cvs-serv21646 Modified Files: addrservlet.rb server.rb tasks.txt Added Files: addrbook.rb addrbook_test.rb addresses.txt addrservlet_test.rb all_tests mockobjects.rb mockobjects_test.rb notes.txt Log Message: Story 1 completed. --- NEW FILE: addrbook.rb --- class AddressBook def initialize( filename, file_store=File ) @entries = Hash.new read_file( file_store, filename ) end def has_key?( name ) @entries.has_key?(name) end def []( name ) @entries[name] end def read_file( file_store, filename ) file_store.open( filename, "r" ) do |file| @entries.clear file.each do |line| name,address = line.split("=") @entries[name] = address end end end end --- NEW FILE: addrbook_test.rb --- require 'test/unit' require 'mockobjects' require 'addrbook' class MockFileStore < MockObject def initialize @name = Expectation.new("name") @mode = Expectation.new("mode") @open_proc = Expectation.new("open_proc") @open_result = nil end def _expect_name( &checks ) @name.expect &checks end def _expect_mode( &checks ) @mode.expect( &checks ) end def _expect_open_proc( &checks ) checks = proc {|value|} if checks == nil @open_proc.expect( &checks ) end def _setup_open( result ) @open_result = result end def open( name, mode, &proc ) @name.actual = name @mode.actual = mode @open_proc.actual = proc if proc proc.call( @open_result ) @open_result.close else return @open_result end end end class MockInputStream < MockObject include Test::Unit::Assertions def initialize( *lines ) @lines = lines @closed = false end def each @lines.each { |line| yield line } end def close @closed = true end def _verify assert( @closed, "closed" ) end end class AddressBookTest < Test::Unit::TestCase FILENAME = "FILENAME" def test_open 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) 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 end --- NEW FILE: addresses.txt --- Nat Pryce=nat...@so..., Steve Freeman=st...@so... Jeff Martin=cus...@so... --- NEW FILE: addrservlet_test.rb --- require 'test/unit' require 'webrick' require 'mockobjects' require 'addrservlet' class MockRequest < MockObject def _setup_query_string( query ) @query_string = WEBrick::HTTPUtils::escape_form(query) end attr_reader :query_string end class MockResponse < MockObject include Test::Unit::Assertions def initialize @headers = ExpectationHash.new("headers") @body = Expectation.new("body") end def _expect_header(header,&proc) @headers[header].expect(&proc) end def []( header ) @headers[header].actual end def []=( header, value ) @headers[header] = value end def _expect_body( &proc ) @body.expect &proc end def body=( value ) @body.actual = value end 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" def set_up @request = MockRequest.new @response = MockResponse.new @book = MockAddressBook.new( NAME1 => ADDR1 ) @servlet = AddressBookServlet.new( {}, @book ) end def test_no_address_found @request._setup_query_string( "UNKNOWN NAME" ) @response._expect_header("content-type") do |value| assert_match( /^text\/.*/, value ) end @response._expect_body { |text| assert_match( /no address found/, text ) } @servlet.do_GET( @request, @response ) @response._verify end def test_no_address_found_when_no_name @response._expect_header("content-type") do |value| assert_match( /^text\/.*/, value ) end @response._expect_body { |text| assert_match( /no address found/, text ) } @servlet.do_GET( @request, @response ) @response._verify end def test_address_found @request._setup_query_string( NAME1 ) @response._expect_header("content-type") do |value| assert_match( /^text\/.*/, value ) end @response._expect_body { |text| assert_match( /#{ADDR1}/, text ) } @servlet.do_GET( @request, @response ) @response._verify end end --- NEW FILE: all_tests --- #!/usr/bin/ruby require 'test/unit' Dir.glob( File.join( File.dirname(__FILE__), "*_test.rb" ) ) do |file| $stdout.print "loading tests from #{file}\n" require File.basename(file) end --- NEW FILE: mockobjects.rb --- require 'test/unit' class Expectation include Test::Unit::Assertions def initialize( name, actual=nil ) @name = name @check = proc { |value| } @check_set = false @actual = nil @actual_set = false end attr_reader :name def expect( &assertions ) @check = assertions @check_set = true end def actual if @actual_set @actual else flunk("#{@name}: actual value not set") end end def actual=( value ) @check.call(value) @actual = value @actual_set = true end def verify assert( @actual_set, "#{@name} does not verify" ) if @check_set end end class ExpectationHash def initialize( name ) @name = name @elements = Hash.new end attr_reader :name def []( key ) if not @elements.has_key?(key) name = "#{@name}[#{key}]" @elements[key] = Expectation.new(name) end return @elements[key] end def []=( key, value ) self[key].actual = value end def verify @elements.each_value { |expectation| expectation.verify } end end class MockObject def _verify self.instance_variables.sort.each do |varname| value = self.instance_eval(varname) if value.respond_to? :verify value.verify end end end end --- NEW FILE: mockobjects_test.rb --- require 'test/unit' require 'mockobjects' class ExpectationTest < Test::Unit::TestCase def set_up @expectation = Expectation.new("tested expectation") end def test_expectation_name name = "NAME" assert_equal( name.to_s, Expectation.new(name).name ) end def test_verify_passes_if_nothing_called @expectation.verify end def test_verify_fails_if_expectation_but_no_actual @expectation.expect { |value| } assert_raises Test::Unit::AssertionFailedError do @expectation.verify end end def test_no_check_defined @expectation.actual = "ACTUAL" @expectation.verify end def test_verify_succeeds_if_expected_and_actual @expectation.expect { |value| } @expectation.actual = "Some value" assert_nothing_raised do @expectation.verify end end def test_actual_value_stored actual_value = "ACTUAL" @expectation.actual = actual_value assert_same( actual_value, @expectation.actual ) end def test_failure_if_actual_value_queried_before_it_is_set assert_raises Test::Unit::AssertionFailedError do @expectation.actual end end def test_expectation_check_Called check_called = false actual_value = "VALUE" @expectation.expect do |value| assert_equal( value, actual_value ) check_called = true end @expectation.actual = actual_value @expectation.verify end end class ExpectationHashTest < Test::Unit::TestCase def set_up @hash = ExpectationHash.new("hash") end def test_empty_hash_verifies @hash.verify end def test_index_creates_new_named_expectation expectation1 = @hash[:KEY1] expectation2 = @hash["KEY2"] assert_instance_of( Expectation, expectation1 ) assert_equal( "#{@hash.name}[KEY1]", expectation1.name ) assert_instance_of( Expectation, expectation2 ) assert_equal( "#{@hash.name}[KEY2]", expectation2.name ) end def test_index_returns_same_expectation assert_same( @hash[:KEY], @hash[:KEY] ) end def test_elements_verified @hash["KEY"].expect { |value| } assert_raises Test::Unit::AssertionFailedError do @hash.verify end end def test_assign_sets_actual_value @hash["KEY"] = "VALUE" assert_equal( "VALUE", @hash["KEY"].actual ) end end class ExpectationStub attr_reader :verify_called def initialize @verify_called = false end def verify @verify_called = true end end class MockObjectTest < Test::Unit::TestCase def test_verify expectation1 = ExpectationStub.new expectation2 = ExpectationStub.new mock = MockObject.new mock.instance_eval do @expectation1 = expectation1 @expectation2 = expectation2 end mock._verify assert( expectation1.verify_called, "expectation1 verify called" ) assert( expectation2.verify_called, "expectation2 verify called" ) end end --- NEW FILE: notes.txt --- 1) No need for mocks to subclass existing interfaces -- dynamic languages don't have or *need* interfaces. This is especially useful when we need to test the interactions between our classes and third party classes, such as those defined by the language and standard library. We don't have to work out how to mock concrete classes that are not implementations of abstract interfaces or final classes. 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). 2) Expectations can be defined using closures, rather than various expectation classes. Closures can call the Test::Unit assertions defined in the TestCase class and access local variables defined in the test method, even though they are called from within a mock object. This makes mock-object classes very flexible, because expectations can be tailored for each test method. 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 *are* factories. So, where in Java we would pass a factory to our objects, so that we could use a factory to create mock objects in our tests, we can pass *classes* to our objects. However, because a class is just an object, in our tests we can create a stub class that returns prepared mock objects, and pass that object to our tested objects instead of a class. To the tested object, the stub responds to the same messages as a class, and so to all intents and purposes *is* a class. 4) NOTE: POTENTIAL PITFALL Servlet used a hash to store names. Changed to use an address book object with the same interface. Address book was tested, but servlet tests were not changed, so servlet still tested against hash object. However, address book didn't implement all methods used by the servlet so servlet failed when run in server. Implemented a mock address book that implemented the same methods as the actual address book, and then changed the servlet tests to use that mock. That caught the error. Alternatively, integration tests (acceptance tests) would have caught the error. LESSON LEARNED: Type system does not help you catch errors at compile time. So, need to be more disciplined in writing your tests. If you introduce a new class, you must write both unit tests for that class *and* a mock for that class, and change the tests for the client classes to use the mock. Keep the mock and the class/unit tests in 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. Index: addrservlet.rb =================================================================== RCS file: /cvsroot/mockobjects/no-stone-unturned/src/ruby/addrservlet.rb,v retrieving revision 1.1 retrieving revision 1.2 diff -u -r1.1 -r1.2 --- addrservlet.rb 31 Aug 2002 10:55:21 -0000 1.1 +++ addrservlet.rb 2 Sep 2002 14:53:00 -0000 1.2 @@ -1,38 +1,38 @@ - -require 'webrick' - - -class AddressBookServlet < WEBrick::HTTPServlet::AbstractServlet - def initialize( config, address_book ) - super - @address_book = address_book - end - - def do_GET( request, response ) - response['content-type'] = 'text/plain' - - query = request.query_string - if query == nil - respond_no_match( response ) - else - name = WEBrick::HTTPUtils::unescape_form( query ) - find_address( name, response ) - end - end - - def find_address( name, response ) - if @address_book.has_key?(name) - respond_address( response, @address_book[name] ) - else - respond_no_match( response ) - end - end - - def respond_address( response, address ) - response.body = address - end - - def respond_no_match( response ) - response.body = "no address found" - end -end + +require 'webrick' + + +class AddressBookServlet < WEBrick::HTTPServlet::AbstractServlet + def initialize( config, address_book ) + super + @address_book = address_book + end + + def do_GET( request, response ) + response['content-type'] = 'text/plain' + + query = request.query_string + if query == nil + respond_no_match( response ) + else + name = WEBrick::HTTPUtils::unescape_form( query ) + find_address( name, response ) + end + end + + def find_address( name, response ) + if @address_book.has_key?(name) + respond_address( response, @address_book[name] ) + else + respond_no_match( response ) + end + end + + def respond_address( response, address ) + response.body = address + end + + def respond_no_match( response ) + response.body = "no address found" + end +end Index: server.rb =================================================================== RCS file: /cvsroot/mockobjects/no-stone-unturned/src/ruby/server.rb,v retrieving revision 1.1 retrieving revision 1.2 diff -u -r1.1 -r1.2 --- server.rb 31 Aug 2002 10:55:21 -0000 1.1 +++ server.rb 2 Sep 2002 14:53:00 -0000 1.2 @@ -1,14 +1,13 @@ -#!/usr/bin/ruby - -require 'webrick' -require 'addrservlet' - -server = WEBrick::HTTPServer.new( :Port => 2000 ) -server.mount( "/address", AddressBookServlet, - "Nat Pryce" => "nat...@so...", - "Steve Freeman" => "st...@so...", - "Jeff Martin" => "cus...@so..." ) - -trap("INT") { server.shutdown } - -server.start +#!/usr/bin/ruby + +require 'webrick' +require 'addrbook' +require 'addrservlet' + +server = WEBrick::HTTPServer.new( :Port => 2000 ) +server.mount( "/address", AddressBookServlet, + AddressBook.new("addresses.txt") ) + +trap("INT") { server.shutdown } + +server.start Index: tasks.txt =================================================================== RCS file: /cvsroot/mockobjects/no-stone-unturned/src/ruby/tasks.txt,v retrieving revision 1.1 retrieving revision 1.2 diff -u -r1.1 -r1.2 --- tasks.txt 31 Aug 2002 10:55:21 -0000 1.1 +++ tasks.txt 2 Sep 2002 14:53:00 -0000 1.2 @@ -21,6 +21,8 @@ REFACTORING + - refactor code + - refactor tests to use expectation and mockobject classes 1c) Retrieve the entries from a file, specified as a servlet property. |