[rspec-users] Ordering in view specs using have_tag and with_tag

Matt Deiters mdeiters at msn.com
Thu May 24 10:24:17 EDT 2007


Hey Wincent,

A couple of questions/comments. Are you doing some sorting logic in the 
view? If so I can see why you would want to assert order, but perhaps it 
would be better to move that type of logic out of view and into your 
controller (or perhaps a presenter or helper). Then you would test the order 
of your collection in one of those (helper/controller) types of specs. Then 
your view would just iterate of the collection and if your view is just 
iterating over the collection, then when your testing order you would really 
be testing does 'ruby iterate right', which again is something we don't want 
to test in our views (or need test at all).

Matt Deiters

>From: Wincent Colaiuta <win at wincent.com>
>Reply-To: rspec-users <rspec-users at rubyforge.org>
>To: rspec-users <rspec-users at rubyforge.org>
>Subject: Re: [rspec-users] Ordering in view specs using have_tag and 
>with_tag
>Date: Thu, 24 May 2007 15:25:26 +0200
>
>Ok, following up on my posts from a couple of days ago...
>
>The question:
>
> > When writing view specs is there any way to test not only for the
> > presence of tags (have_tag) and nested tags (with_tag), but also test
> > that they appear in a given order?
>
>The workaround:
>
> > One thing which may prove useful is that
> > assert_select returns an array of matches, and also passes that array
> > into any block (if called with a block). The array contains
> > HTML::Node instances, and these nodes have a position attribute (the
> > position of the node within the byte stream); so making comparisons
> > of order externally is possible and could be down already right now
> > with no changes (although not transparently; you'd have to explicitly
> > compare element positions in your specs).
>
>Here's an example spec showing one way this could be done. It's
>pretty ugly though (and I expect the list server will make it even
>uglier by hard-wrapping it) as it requires quite a bit of "book-
>keeping" to be done:
>
>describe '/users/index with 2 users' do
>    before do
>      users = (1..2).inject([]) do |list, index|
>        user = mock("user #{index}")
>        user.should_receive(:login_name).and_return("example login
>name #{index}")
>        user.should_receive(:display_name).and_return("example display
>name #{index}")
>        user.should_receive(:email_address).and_return("example email
>address #{index}")
>        list << user
>      end
>      assigns[:users] = users
>      render 'users/index'
>    end
>
>    it 'should display the login names, displays names and email
>address, in alternating odd and even rows' do
>      odd = even = nil
>      response.should have_tag('div.odd>div') do |match|
>        odd = match[0]
>        positions = []
>        with_tag('div.label', 'Login name:')            { |match|
>positions << match[0].position }
>        with_tag('div.info', 'example login name 1')    { |match|
>positions << match[0].position }
>        with_tag('div.label', 'Display name:')          { |match|
>positions << match[0].position }
>        with_tag('div.info', 'example display name 1')  { |match|
>positions << match[0].position }
>        with_tag('div.label', 'Email address:')         { |match|
>positions << match[0].position }
>        with_tag('div.info', 'example email address 1') { |match|
>positions << match[0].position }
>        positions.should == positions.sort
>      end
>      response.should have_tag('div.even>div') do |match|
>        even = match[0]
>        positions = []
>        with_tag('div.label', 'Login name:')            { |match|
>positions << match[0].position }
>        with_tag('div.info', 'example login name 2')    { |match|
>positions << match[0].position }
>        with_tag('div.label', 'Display name:')          { |match|
>positions << match[0].position }
>        with_tag('div.info', 'example display name 2')  { |match|
>positions << match[0].position }
>        with_tag('div.label', 'Email address:')         { |match|
>positions << match[0].position }
>        with_tag('div.info', 'example email address 2') { |match|
>positions << match[0].position }
>        positions.should == positions.sort
>      end
>      odd.position.should < even.position
>    end
>end
>
>Compare this to the version without the book-keeping:
>
>describe '/users/index with 2 users' do
>    before do
>      users = (1..2).inject([]) do |list, index|
>        user = mock("user #{index}")
>        user.should_receive(:login_name).and_return("example login
>name #{index}")
>        user.should_receive(:display_name).and_return("example display
>name #{index}")
>        user.should_receive(:email_address).and_return("example email
>address #{index}")
>        list << user
>      end
>      assigns[:users] = users
>      render 'users/index'
>    end
>
>    it 'should display the login names, displays names and email
>address, in alternating odd and even rows' do
>      response.should have_tag('div.odd>div') do
>        with_tag('div.label', 'Login name:')
>        with_tag('div.info', 'example login name 1')
>        with_tag('div.label', 'Display name:')
>        with_tag('div.info', 'example display name 1')
>        with_tag('div.label', 'Email address:')
>        with_tag('div.info', 'example email address 1')
>      end
>      response.should have_tag('div.even>div') do
>        with_tag('div.label', 'Login name:')
>        with_tag('div.info', 'example login name 2')
>        with_tag('div.label', 'Display name:')
>        with_tag('div.info', 'example display name 2')
>        with_tag('div.label', 'Email address:')
>        with_tag('div.info', 'example email address 2')
>      end
>    end
>end
>
>So, yes, the book-keeping-free version is much easier to look at, but
>it's also far too easy for the spec to pass with faulty input (jumble
>the divs around in any order you want and it will still pass). So
>improving have_tag, with_tag, or the underlying assert_select seems
>to be a very worthwhile project; I've got a very basic prototype
>implementation in place based on this small patch to actionpack/lib/
>action_controller/assertions/selector_assertions.rb:
>
>=== vendor/rails/actionpack/lib/action_controller/assertions/
>selector_assertions.rb
>==================================================================
>--- vendor/rails/actionpack/lib/action_controller/assertions/
>selector_assertions.rb     (revision 7191)
>+++ vendor/rails/actionpack/lib/action_controller/assertions/
>selector_assertions.rb     (local)
>@@ -286,6 +286,13 @@
>               "Expected at most #{equals[:maximum]} elements, found #
>{matches.size}."
>           end
>
>+        if !matches.empty?
>+          if @previous_match and matches[0].position <
>@previous_match.position
>+            raise "selector order mismatch ('#{matches[0].to_s}' at
>position #{matches[0].position} appears before '#
>{@previous_match.to_s}' at position #{@previous_match.position})"
>+          end
>+          @previous_match = matches[0]
>+        end
>+
>           # If a block is given call that block. Set @selected to allow
>           # nested assert_select, which can be nested several levels
>deep.
>           if block_given? && !matches.empty?
>
>Experimentation shows that the dynamic class created by RSpec for
>each "it" block receives the assert_select messages and hangs around
>for the duration of the block. So that means instance variables can
>be used to store positional information and an exception can be
>thrown if things don't happen in the expected order.
>
>This works, at least for the sample spec I've posted above. In the
>event of the ordering requirement failing the exception will include
>diagnostic information that looks like this:
>
>selector order mismatch ('<div class="label">Display name:</div>' at
>position 109 appears before '<div class="info">example login name 1</
>div>' at position 150)
>
>Limitations, and doubts:
>
>- I don't know what type of exception to throw, so for now I'm just
>throwing a String (which becomes a RuntimeError)
>
>- no way to make it optional at this stage
>
>- this patch (or anything like it) is unlikely to get accepted in
>Rails because it will probably break lots of existing specs/tests
>which are written without caring about selector ordering
>
>Any further ideas?
>
>Cheers,
>Wincent
>
>_______________________________________________
>rspec-users mailing list
>rspec-users at rubyforge.org
>http://rubyforge.org/mailman/listinfo/rspec-users




More information about the rspec-users mailing list