matchers_in_have_tag_blocks.should be(/with_tag/)
By Phil Matarese on October 15th, 2008
The Setup
When writing test code for Rails apps, I prefer Rspec. For me the spec definition syntax feels more natural. Though, the other day I got tripped up by the Rspec equivalent of assert_select. With standard rails tests, the assert_select method can be nested to help ensure properly structured html elements.
1 # This tests that both users show up in the yes_list.
2 # As opposed to the no_list I guess.
3 assert_select 'div#yes_list' do
4 assert_select 'div#user', /#{@user1}/
5 assert_select 'div#user', /#{@user2}/
6 endRspec uses have_tag as a wrapper to this method, but uses with_tag and without_tag to wrap the method when called inside the nesting. The without_tag allows for simpler negative assertions, in case we wanted to test that user3 was not in the list for example.
1 # This also tests that both users show up in the yes_list.
2 response.should have_tag('div#yes_list') do
3 with_tag('div#user', {:text => /#{@user1}/})
4 with_tag('div#user', {:text => /#{@user2}/})
5 end
Once you know this, I think the spec code looks very clear. Unfortunately, calling have_tag within a have_tag block won't raise an error. There's no mechanism that will warn you that your inner have_tag is ineffective. The results of the inner have_tag will simply be ignored.
The Pitfall
When I first wrote the above spec, I used have_tag and have_text instead of with_tag. And to my naïve eyes, everything looked great. The code ran and the tests passed. Done and done.
1 # This merely asserts the existence of a yes_list,
2 # no matter who's on the list.
3 # Here at the Rails Loft, we're more discerning than that.
4 response.should have_tag('div#yes_list') do
5 have_tag('div#user') do
6 have_text(/#{@user1}/)
7 end
8 have_tag('div#user') do
9 have_text(/#{@user2}/)
10 end
11 endThis was completely wrong. The tests were passing, but they still passed when I removed @user1 from the yes_list one. When used inside another have_tag block, a have_tag's assertions was completely ignored. I think this had something to do with the inner have_tag setting up a new AssertSelect object that then got ignored by the outer AssertSelect object. The reason for my misunderstanding doesn't really matter, though. What matters is that I thought everything was fine; all the tests were passing after all. Luckily, I fiddled with the code a bit before committing, and discovered that reversing an important condition in my application code didn't affect my test one bit. Whoops. This was a mistake that continuous integration and testing metrics could have never caught, which emphasized to me how important it is to be thorough and careful when writing tests.
The Lesson
Always, always, always make sure your tests fail before you make them pass. Your tests are there to ensure proper functionality of application code, and this is the only way you can be certain your test code is doing what you expect. This doesn't necessarily mean you always have to use test-first development, though you should certainly give TDD a try. Making your tests fails could be as simple as toggling a conditional in your application code or changing the id of a tag against which you're matching. This way you'll be confident that your tests are truly working the way they appear to work.

Comments