advertisement

Print

Cookin' with Ruby on Rails - Integration Tests
Pages: 1, 2, 3, 4, 5, 6, 7

Boss: I like the way Rails lets us structure these into more than one level. I'm sure any of our business folks will be able to look at that top level test and get comfortable with their understanding of what's being tested. And I'm just as sure that most of them wouldn't be comfortable at all with their understanding of the low-level test you just wrote.

Paul: They wouldn't be alone, Boss. You mind walking me through those new test methods, CB?

CB: Sure, Paul. Let's take a closer look.

our new methods
Figure 20

Remember that the verify_a_recipe_can_be_deleted method is called from within our test_the_homepage method. That's important because verify_a_recipe_can_be_deleted assumes that the page has already been retrieved and is in-scope. The verify_pizza_row and assert_select methods would all fail if that weren't true.

We've already talked about the verify_pizza_row code a bit, so let's start at the next line. I'm adding one to the count of recipe records to account for the row of headings on our table. It's not technically necessary, but since Boss wanted to focus on the page I thought it would be better from a communication perspective. Next we send a post request to our app to delete the pizza record. We used a post instead of a get because we've got code in our controller to make sure that any requests that'll change the state of the database are post-type requests. If we didn't take that precaution, Boss, Google and other web-crawlers could create problems for us just by following our site's links. When a recipe record gets deleted from our database, our code does a redirect_to to the list action. The three lines that follow the post pretty much mirror the code in the controller. The last couple of lines are where things might seem a little tricky. The assert_select line uses the :count option to verify that when the page gets rendered after the deletion, it has one less row than it did before. The last line is where we make sure that the row with the link to the pizza recipe is the one that actually got deleted. Paul, you might remember that we used assert_nothing_raised in our Unit tests to make avoid crashing our test case with the method that tested to make sure we couldn't delete a Category if there were Recipes assigned to it.

Paul: I do remember that, now. It trapped the exception that MySQL raised when we tried to do that.

CB: Exactly. And that's what we're doing here too. When we assert that there's an item on the page that's not there, the Test Framework raises an exception that's translated into a failure. We're testing a negative condition here: we want to test that an item is not on the page. So, we use assert_raise to verify that the exception is raised as expected. Of course, if the business folks wanted to see how we're verifying that a record can be deleted, but this level of detail was confusing to them, we could easily just move this code down a level; maybe create a new method we'd call from here with a name like verify_pizza_row_no_longer_appears_in_the_table. And we could just as easily move the check for the header row down a level too. It's just a matter of understanding what level of detail our customers are comfortable with.

Boss: That's pretty powerful stuff, CB. I like it. Communication is definitely key. And I have to agree with what Paul said earlier. I think we could have avoided a lot of this last mess we just had to deal with if we'd created automated tests up front in our development process to get this level of communication happening early.

CB: Excellent! I'm really glad to hear you say that, Boss. Let's take another look at our home page and see what else we need to test from there.

another look at our home page
Figure 21

I think we probably ought to talk just a bit about how I'm thinking we'll structure the Integration tests. I like to approach it a page at a time; first verifying that everything that's supposed to be there is there. Then I like to verify the working of the things on that page that're going to leave us on or bring us back to the same page So it makes sense to me to include the verify_a_recipe_can_be_deleted method in test_the_home_page. Clicking the Category links also leave us on this page; just showing a different set of recipes, so it makes sense to me to test them in our test_the_home_page method too. What do you think, Boss?

Boss: Yeah. I like that. It's pretty much the way I'd do it manually; make sure everything I expect is there, then test to make sure they work, then move on to another page and do it again.

CB: OK. So we've verified the content of the home page. Now, let's add another test to verify our Category filter. When we click on a Category link, the page is supposed to get refreshed showing only the recipes for that category. I could included this in the test_the_home_page method, but since we've already modified the content of our test database by deleting a record, I'm going to create a new test method so that we start fresh. So Boss... how should we test that behavior?

Boss: Well, I'd say you need to start with having more than one category on the page, then select one of them, and then verify that only that category is on the page afterwards,

CB: Excellent. So here's my take on the high-level test for the category filtering functionality.

def test_category_filtering
  browse_to_the_home_page
  verify_both_categories_present
  select_one_category
  verify_only_that_category_is_present
end

What do you think, Boss?

Boss: That looks good.

CB: Cool. Then I'll add the lower-level test methods.

def verify_both_categories_present
  assert_select "tr" do
   assert_select "td" do
     assert_select "a", {:text=>"main course", :count=>1}
     assert_select "a", {:text=>"beverages", :count=>1}
   end
  end
end
def select_one_category
  get "recipe/list?category_id=1"
  assert_response :success
  assert_template "recipe/list"
end
def verify_that_only_that_category_is_present
  assert_select "tr" do
   assert_select "td" do
    assert_select "a", {:text=>"main course", :count=>1}
  assert_raise(Test::Unit::AssertionFailedError) {assert_select "a", {:text=>"beverages"}}
   end
  end
end

And now we run our test...

results of testing our filtering
Figure 22

CB: Houston, we have a launch! ;-)

Seriously, though, I think we're just about done with the home page. The only other link on the page that's going to leave us back here is the "Show all recipes" link. Since we know that browsing to the home page currently takes us to the recipes controller index method, I'm not sure we really need to test that. What do you think, Boss?

Boss: I think we need to test it. First, I'd like to make sure that we test the home page very thoroughly. And it just occurred to me that we didn't make sure that all the recipes were shown in our test of the home page contents. So I definitely think we need to test this. And I want to make sure that the number of recipes in the database gets compared to the number of recipes shown on the page.

CB: Not a problem, Boss. How about this?

def test_show_all_recipes_link
  browse_to_the_home_page
  click_the_show_all_recipes_link
  verify_all_recipes_shown
end

Boss: That looks good. I see you're reusing the browse_to_the_home_page method. That's good. I'm already comfortable that I know what it does. What do the other ones look like?

CB: Well, the click_the_show_all_recipes_link is real simple. From a technical perspective, it's really not worth putting into a separate method. But our Integration tests are as much about communication with the business folks as they are about testing. So, I thought I'd go ahead and put the one-liner into its own method just to keep things at the same level.

def click_the_show_all_recipes_link
  get "recipe/list"
end

The other method is going to address the concern you just expressed that we're actually testing that all the recipes in the database get displayed on the page.

def verify_all_recipes_shown
  recipe_count = Recipe.count
  header_row = 1
  assert_select "tr", :count=>(recipe_count + header_row)
end

First I get the count of all the recipes in our database. The second line is just for clarity; reminding us that one of the rows in the table is a header row. And then we compare the number of rows displayed against the number of recipes in the table. What do you think? Will that do it?

Pages: 1, 2, 3, 4, 5, 6, 7

Next Pagearrow