O'Reilly    
 Published on O'Reilly (http://oreilly.com/)
 See this if you're having trouble printing code examples


Cookin' with Ruby on Rails - More Designing for Testability

by Bill Walton
07/26/2007

NOTE TO READER: This tutorial is intended for use with Rails 1.2.6. It has not yet been updated to support Rails 2.x.

CB: Hi, Paul. You ready to get started on our Functional tests?

Paul: You bet, CB. But I've been thinking about the Unit tests we worked on last time, and I have this nagging feeling that maybe we called them done too soon. Before we get started on the Functional tests, would you mind if we took a quick look at the app?

CB: Of course not! Let me get it fired up. What's bugging you?

Paul: Remember when we came across that problem (Figure 22, "Cookin' with Ruby on Rails: Designing for Testability") with the categories fixture not loading because there were records in the recipes table? You reminded me that we'd actually seen that problem before when we were working on the app with Boss. You said you'd put a quick fix in place that we really needed to revisit. We fixed the fixture loading problem, but we never revisited that code. I'm thinking we ought to take a quick look at that and see if we're really done with our Unit tests.

CB: I'm glad you reminded me, Paul. I do remember now. I think I said something like, "let's tackle it in two pieces," but we got focused on getting the tests working and I completely forgot to go back and do the second part. If I remember right, we were talking about the code that prevented visitors from deleting a category that had recipes assigned to it. Let's take a look at the code in our category controller.

our "don't delete categories with recipes" hack
Figure 1

Yep. I'd say we need to fix this now, before it gets any worse. What we're doing here works, but it really ought to be handled in the model instead of the controller.

Paul: Why do you say that? That it ought to be handled in the model, I mean?

CB: Mainly because this is business logic. The question we need to come back to Boss with is: "How do you want to handle it if a visitor tries to delete a category that has recipes?" There are lots of technical options. We could delete the category and all the recipes under it. We could have some sort of default category like "Unassigned" and re-assign all the recipes for the category being deleted. Maybe he only wants to allow certain visitors to delete categories. The point is, that's his call because those are business decisions. And in the MVC pattern, business logic belongs in the model. Another way to think about it is that this is really a validation. Last time we got together, we put validations in place to prevent category objects from being saved to the database unless they met certain conditions. This is a validation to prevent category records from being deleted from the database unless they meet certain conditions.

Paul: Cool. I was pretty much thinking the same things, but I wanted to make sure we were on the same page. So, how do we proceed?

CB: Let's start by doing a reset on where we are. Let's rerun our Unit tests for the category model. I'll open a command window and...

ruby test\unit\category_test.rb

rerunning the category Unit tests
Figure 2

Note to reader: If you get errors when running the category Unit test at this point, it probably means you have leftover recipe records in your test database. Run recipe_test.rb to remove them.

Now let's take the wrapper off that code in the category controller so we'll have an app that breaks.

wrapper removed
Figure 3

And now let's check our app. Start Mongrel, browse to http://localhost:3000/category/list, and then try to delete one of the categories.

Yep, we broke it.
Figure 4

CB: Yep. We broke it. So now, let's rerun our tests.

Oops. Our tests don't catch it.
Figure 5

Paul: So our app's broke, but our tests don't catch it.

CB: Exactly. If we'd written our tests before we wrote the code, we probably wouldn't be here. The truth is, though, there'll always be times when we'll miss a test we should have written and have to go back and add it after we've found a new way to crash our app. But writing the tests first, thinking it through before we write the code, will make it happen a lot less than doing it after the fact. So let's write a failing test that shows us the problem and tells us how to fix it.

Let's think this through a little before we start writing our test. We want to make sure we write a test that ensures our app works the way we intend it to work. We need a test to make sure that a category record that has a child record can't be deleted. Problem is, from a test perspective, we've made sure that there aren't any child records. We added a teardown method to our recipe fixture to make sure that all our recipe records got cleared out of our test database after the recipe tests were run. Otherwise, our category fixture wouldn't load. So, to start, we're going to have to make sure the category record we're trying to delete has a child recipe.

We know from the error we've already seen that, when we try to delete the category record, Rails is going to raise an exception. In Rails, some exceptions are worse than others. If we try to find a record that doesn't exist, for example, Rails raises a RecordNotFound exception. We can use that if we want, or we can just check the result to see if it's empty. The StamentInvalid exception we're getting here crashes our app, and it'll crash our tests too. Luckily, Rails gives us a built-in assertion to handle it in our tests so that the test method reports a failure instead of crashing the test case.

First, let's open up the categories fixture and pick a record.

records to pick from
Figure 6

CB: I think I'll use the beverages record. So, now I'll open up test\unit\category_test.rb and add a new test method.

def test_cannot_delete_record_with_child

   category_with_child = Category.find_by_name("beverages")
   assert_not_nil category_with_child
 
   new_recipe = Recipe.new(:title => "test drink",
                            :category_id => category_with_child.id)
   new_recipe.save
   recipe_exists =
            Recipe.find_by_category_id(category_with_child.id)
   assert recipe_exists
 
   assert_nothing_raised(ActiveRecord::StatementInvalid) {category_with_child.destroy}
   category_with_child = Category.find_by_name("beverages")
   assert_not_nil category_with_child

   new_recipe.destroy
end


Note to reader: The format for the assert_nothing_raised method is: assert_nothing_raised (exception) {block}. Make sure that {category_with_child.destroy} appears on the same line as the rest of the method in your code, not broken onto a second line as shown above.

Paul: So it looks like the "meat" is that third section that starts with assert_nothing_raised. Is that right?

CB: Exactly. We want our make our app work so that, if a visitor attempts to delete a category record that has a child record, no exception is raised to crash the app. The first two sections are set up. I get a category record and test to make sure I've gotten the one I wanted. Then I create a recipe child record and test to make sure that it got created. Then comes the meat where I try to delete the record, which we know from running the app will raise the StatementInvalid exception. The assert_nothing_raised assertion traps the exception so that the test will pass or fail, not crash the test case. Then I test again, just to make doubly sure that the category record is still there. Finally, I delete the recipe record I just created. If I don't delete that record, I'll have trouble on the next test method when Rails tries to load the categories fixture with a record in the recipe table. So, now we're ready to run our category Unit test.

ruby test\unit\category_test.rb

caught the failure
Figure 7

Paul: Wow!!!

CB: Don't worry. It's not as bad as it looks. In fact, it's pretty much exactly what we wanted. We've got a failing test! Take a closer look, starting at the top. Our test_cannot_delete_record_with_child ran first and failed right where we wanted it to fail: at assert_nothing_raised. If you look closely, you'll see that each error after that is associated with a different test method. Rails stops the execution of a test method as soon as it encounters either a failure or an error. When we got the failure we were looking for, Rails stopped the execution of that method. That meant that the child record never got deleted and, as we already knew would happen, the categories fixtures can't load. That's what all the errors are about. So, let's write some code to fix our failure! Let's open our category model.

the category model
Figure 8

CB: What we need to do here is check, before we try to destroy a record, that it doesn't have any child records. I'll use one of the standard Rails Callbacks: before_destroy. If a before_ Callback returns false, the associated action is canceled. So, first I'm going to add the call to the Callback...

before_destroy :check_for_children

And then I'll add the Callback method itself...

def check_for_children
   recipes = Recipe.find_all_by_category_id(self.id)
   if !recipes.empty?
     return false
   end
end

So we end up with...

our new category model
Figure 9

CB: And now, before we forget, let's make sure there aren't any recipe records in the database to cause us problems again. We could just rerun our recipe test and let the teardown method clean things up. But now that we've seen what happens when we do things out of order, I think we ought to address the "order" problem. I put the teardown in the Unit test for recipes because that's where the records were getting created. But our problem really isn't that recipe records exist in the table at the end of the recipe test. Our problem happens when recipes exist at the beginning of the category test. So, what say we move the code to remove them from the teardown method in recipe_test.rb to a setup method in category_test.rb ?

Paul: Sounds good to me.

CB: Yeah, me too. So, I'll copy the teardown method from recipe_test.rb to category_test.rb and rename it...

def setup
   recipes = Recipe.find(:all)
   recipes.each do |this_recipe|
     this_recipe.destroy
   end
end

We need to add the recipes fixture...

fixtures :categories, :recipes

And then we'll rerun our category test,

ruby test\category_test.rb

and...

we did it!
Figure 10

CB: Ta daaa!!!

Paul: Hold on, CB. We went down that road before. Why don't we just make sure our app's fixed this time?

CB: No problem, Paul. You still got Mongrel running? OK. Go ahead and browse to our category list page and try deleting one again.

Paul: OK. OK. It's not deleting the category, and it's not crashing, so it looks like we've got it taken care of. But now I'm wondering, if I've got to test it in the browser anyway, why spend the time to create the Unit test?

CB: That's a good question, Paul. We found out that we were missing a test by looking at the app from a higher level. And since we found the problem at the higher level, we wanted to go back there to make sure that what we'd done had actually fixed it. That's pretty typically going to be the case. But now we know that we've got a low-level test that catches the problem. So we don't have to test it again at the higher level. We can if we want to, and I probably will because I think of test suites like a layered defense, but the value of doing it again at that higher level will primarily be in making sure our test suite's not broken rather than making sure the app's not broken. It'll be easier to show you. If you're ready, I think we should get started on our Functional tests.

Paul: I'm ready. Let's do it.

CB: Cool. Let's take a look at the Functional test stub that Rails produced for us. Open up test\functional\category_controller_test.rb.

the category controller test stub
Figure 11

Paul: That looks a lot more complete than the Unit test scaffolds. More like the scaffolding Rails produces for the app itself.

CB: Yep. I think the reason Rails doesn't do more with the Unit test scaffold is, like I said before, the model is where we put validations and business logic. Rails can't predict a whole lot about what we'll want to do with those. But since it's generating the basic CRUD functionality in scaffold code for the controller, it can generate the tests for that. We've got test methods for all of the default methods in the controller, plus a setup method to prepare for each test method execution.

Paul: Yeah. I can see it's setting up four instance variables. But I only see one of them getting used in any of the methods. How're the other ones getting used?

CB: Now, Paul. You're making me think you didn't do your homework. ;-) Remember the links I gave you last time? Lemme refresh your memory. The author of "Testing the Rails," a fellow by the name of Evan "Rabble" Henshaw-Plath, called these...

quoting from Testing the Rails
Figure 12

Paul: OK, I do remember that now. But I really didn't understand what he meant by "simulate a web request." I thought the test framework was sending requests to Mongrel and then looking at the replies. Is that not what's going on?

CB: Actually, no. That's not what's going on at all. I realize we had Mongrel running during our last session, so it's reasonable that you interpreted things that way. And there are other frameworks that do work that way. I had an email exchange with a fellow named Philip Plumlee who's written a Short Cut that we'll probably be talking about if Boss has ideas for this app that require us to use Ajax. I thought Phlip (that's the screen name he uses on the forums) explained it pretty well. He said...

expaining Rails' use of the Mock the Webserver pattern
Figure 13

Paul: So, we don't need to be running Mongrel to run our tests?

CB: Nope. Here. Let's prove it. Shut down Mongrel and then close the command windows and the browser. So, now I've got the Instant Rails manager and the MySQL server running, but nothing else. So, let's open a new command window and give this Functional test scaffold code a test drive.

Paul: OK.

ruby test\functional\category_controller_test.rb

first run of the category_controller_test
Figure 14

Paul: Ouch! I can see that Mongrel didn't need to be running, but what's that about? That's not because of the changes we made to the code, is it? I mean, it looks like all the tests failed, and we only changed one method in the category controller.

CB: Nope. The error message says Rails can't find a fixture named "first." If we ran the recipe controller test we'd get the same results. I could have avoided this but I wanted you to see it. We haven't worried about fixture names yet because, where we are right now, we haven't needed to pay them any real attention. Later on, though, fixture names are going to become a lot more important in terms of the readability of our tests. This is only speculation on my part, but I think this is sort of an example of what DHH calls "syntactic vinegar." Rails generated both the Unit and Functional tests at the same time and both use the same fixtures. So, I'm pretty sure Rails knows that the fixtures are named "one" and "two." ;-) This is, I think, a message from DHH and team: "Scaffolding is not an excuse not to think. Do you want to use the same fixtures for your Unit and Functional tests? Or do you need different fixtures? How are you going to name your fixtures so that your code communicates your intent?" Those sorts of questions are important, and you and I are going to spend some time talking about them in the very near future. Right now, though, let's just fix the test case so we can bring you up to speed on the basics. You OK with that?

Paul: Yeah. I think that's a good idea. I'm not sure I could contribute much to a discussion on those topics without understanding at least the basics.

CB: Cool. The fix is easy. Take a look at the setup method on category_controller_test.rb again. The culprit is the line at the bottom of it that says:

@first_id = categories(:first).id

We need to change that to:

@first_id = categories(:one).id

And since we already know that our model validation is going to fail if we try to save a category record without a name, let's go ahead and fix the test_create method to avoid that. Change...

post :create, :category => {}

to

post :create, :category => {:name => "new category"}

Now, let's rerun our category controller test. Then I'll walk you through what's going on.

ruby test\functional\category_controller_test.rb

a successful run of our category_controller_test
Figure 15

Paul: The tests all passed this time. That's good. But what are those warnings about?

CB: Those are telling us that somewhere, either in our application or in Rails itself, there's code that's using POST on a hyperlink and that Rails is not going to support that in the future. Rails is headed toward full support for the REST pattern, so, if you're not already familiar with it, you might want to get familiar with it. There's a good intro at http://doc.opengarden.org/REST. And, as with all things Rails, Google is your friend ;-)

In this case, there's nothing wrong with our app. The warnings above are being generated by some Rails internal code. Some folks find them a little annoying, but I think it's probably good to have the reminders.

So now let's talk about what's going on in those test methods. Let's start right at the beginning of our app's execution cycle. To get a complete view of what's being tested and how, we need to take a look at the index method in our controller, the test_index method in our controller test case, and the setup method that gets run prior to all our test methods.

the controller, test, and setup for the index method
Figure 16

CB: Remember that before each of the test methods in our test case is executed, our setup method is going to run. Like we talked about a minute ago, the first three lines of that method create a set of objects that are going to be used in pretty much every Functional test we're ever going to write. The first line tells Rails which controller we'll be testing and to create a new instance of it. I can't remember whether we covered this in our first meeting, but, even if we did, it bears repeating: Rails applications live for exactly one request-response cycle. Every request from a browser to a Rails app contains information in the URL about which controller and method are being requested. Rails creates a new instance of the controller being requested for every request. To execute our tests, since we're "mocking the web server," we need to tell Rails to create a new instance of the controller we're going to be sending requests to. That controller instance expects to receive a request object and expects that it will provide content for a response object. So, we need to create those objects too.

When we execute our test_index test method, the first line tells Rails, actually the test framework portion of Rails, to use the request object we just created and send a request to the index method in our Rails application's category controller using the GET request type. The next line tells the test framework to check the response object for a status code in the 200 to 299 range. A successful status code means that the server successfully received, understood, accepted, and processed the request. The last line tells the test framework to check to make sure the page "being sent back to the browser" was rendered using the list.rhtml template. I say "being sent back" in quotes because, remember, we're not actually going to send anything anywhere. The test framework is going to fake Rails into believing it's talking to a web server as normal, but the framework is actually sending all the requests and capturing all the responses.

How's that feel?

Paul: I'm pretty comfortable with it, I think. Let's take a look at another one.

CB: Sure. Let's take a look at the new and create methods together since we'd normally expect to see them occur in sequence.

the controller, test, and setup for the index method
Figure 17

CB: In the new method in the controller, we create a new instance of the category class. That instance variable gets passed to the default view which; in this case, is new.rhtml. To test this, our test_new method uses the request object created by the setup method to send a GET request to the new method in our category controller. Then, as in the test_list test method we just looked at, the test framework looks at the response that Rails creates. It checks to make sure the status code is set to one of the Success codes and to make sure that the correct template was used to create the HTML page that's being returned. The next line in our test method is one we didn't see in test_list.

assert_not_nil assigns(:category)

This line tests to make sure that the new method in the controller actually created an instance variable named @category as expected.

Paul: Seems like it should say something like...

assert_assigns @category

CB: From a readability perspective, that's not a bad idea at all. Especially if we were going to be reviewing this with someone like Boss. In fact, since I've already showed you how easy it is to create custom assertions, I'd say that'd make a great homework assignment! ;-) When you do it, you'll need to know that assigns is a hash, one of four that gets created by the test framework for every response it gets from Rails. We'll cover the others as we get to them. For now, it'll do to know that :category is a key in the assigns hash, that a key-value pair will be created in that hash for each instance variable created in the method, and that the values of the instance variables will be accessible via standard Rails dot notation. As it stands, our test is only checking to see that a variable has been created by the controller method. In just a minute, we'll see how to check to make sure its values have been assigned as expected. But for now, we can see that we're testing for everything we asked Rails to do in the new method.

So now let's look at the test_create method. The first and last lines should look familiar. We used them in our Unit tests to make sure that a record had actually been saved to the database. That's exactly what they're doing here. The second line in the method is creating a request, this time a POST request, and passing in a hash. In normal operation, this is the value that would be passed to Rails via the params hash when a client submits a request. Take a look at the create method in the controller. The first line is...

@category = Category.new(params[:category])

The hash in the second line of the test_create method...

:category => {:name => "new category"}

creates the key-value pair that's put in the request object and passed in params[:category] to the create method in the controller.

The next two lines in the test method verify that the status code Rails is sending "back to the browser" is a redirect, and that it's redirecting to the list method in the same controller.

CB: You OK with that?

Paul: Yeah. I think I'm following you.

CB: Cool. So... what's missing?

Paul: The test method is only testing the success path. The controller method has two paths; one for a successful save and a different one for an unsuccessful save. Our test method needs work.

CB: Would you like to lead? Or shall I?

Paul: Allow me. Please ;-)

I guess the first thing I need to decide is whether to put this inside the test_create method or to write a new method. If I put it inside the test_create method, I'll save some test execution cycles since the setup method won't have to run again for a new method. But if I do that, then anybody reading the tests will have to dig a little harder to see that I'm testing for failure too. What do you think, CB?

CB: Well, I tend toward making it as easy as possible to grok the functionality of the app from the test cases. But in this case, I think we could accomplish both goals if we put the test inside the existing method, but rename the method so that a reader would get what's being tested just from the name. How 'bout maybe renaming test_create to test_create_success_and_failure ?

Paul: I like it. OK. So I'll add some code to the existing test method. Let's take another look at the create method in the category controller and the test_create method in the test case.

create and test_create methods
Figure 18

Paul: Well, the most obvious difference between a successful create and a failed create is that, on a successful create the number of records in the table will increase. That's a "Duh" and the scaffolded test code includes that test. It doesn't test to make sure the number doesn't increase on a failed create though. And looking at the controller method again, it looks like there's something that happening on a successful create that isn't being tested yet. What's the flash[:notice] line do? And can we test it?

CB: The flash object is sort of a general purpose way to send messages back to the browser. The most common, and many would say most appropriate, use of the flash object is to send back success messages like this. If a save fails, for example, the validation automatically adds a message to the errors object and, typically, in the associated view file, we'll have a line that renders the messages with error_messages_for 'some_specific_object'. The use of the flash object, on the other hand, is common enough that the scaffolding automatically puts the line to render it in the application layout file, app\views\layouts\application.rhtml, so that it's available for every page we render. And, yes, we can test for it. Remember just a minute ago I said that assigns was one of four hashes constructed for every response? Well, flash is another one of those four. And we test it pretty much like we tested assigns.

Paul: OK. What about the failure path? If the save is successful, the controller uses redirect_to. But if it fails, it uses render. What's the difference?

CB: That's a good question, Paul. The difference is important. The redirect_to starts a new request/response cycle. It generates a new request, and Rails treats that request just as if it were coming from the browser. That means the controller method gets invoked and then the instance variables that it creates get passed to the view for rendering of the response. In this case, that would mean a new @categories object would be created and everything the visitor had entered would be lost. The render, on the other hand, doesn't invoke another controller method. It tells Rails to use the instance variables that this method created, but render the response using this other template. We have to be careful when we use render to make sure that the instance variables that the template expects all exist. Otherwise, when the visitor submits that page it might not contain all the value we expect it to contain. In this case, the new view only expects @categories. By using render instead of redirect_to we're using the @categories object we just created using the information the visitor just entered. Some of that information is incorrect, which caused the validations to fail, and we're passing it all back to them so they can correct the problems. If you want to fire up the app and enter a name for the category that's longer than 100 characters so it fails our validation, you'll see what I mean. ;-)

Paul: That's OK. Maybe later. I think I understand the difference. So, to test this, we just need to make sure the correct template's being used. Lemme take a stab at this. I need to add a test for the flash on the success path, and tests to make sure a record didn't get saved and to make sure the right template was used for the failure path. I think our new test method, or rather our newly named test method, needs to look like...

def test_create_success_and_failure
   num_categories = Category.count
   post :create, :category => {:name => "new category"}
   assert_response :redirect
   assert_redirected_to :action =>'list'
   assert_not_nil flash(:notice)
   assert_equal num_categories + 1, Category.count

   num_categories = Category.count
   post :create, :category => {:name => ""}
   assert_response :success
   assert_template 'new'
   assert_equal num_categories, Category.count
end

What do you think?

CB: I think that looks pretty good. Let's try it and see what happens! Let's replace the test_create method with your new one and rerun the test.

ruby test\functional\category_controller_test.rb

results of our replacement test_create method
Figure 19

Paul: Hmm... It says there was an error in the line where we tested the flash object. I don't get it. I tested it exactly the same way the scaffolding tested assigns in the new method. What gives?

CB: The assigns hash is a little different from the others. It's a "historical" thing and, frankly, I have a bit of a problem remembering all the differences between the rules for accessing assigns versus the other hashes. I've found that in most cases I can get by with just remembering one. For assigns I use parentheses, and for the others I use the standard hash notation, which is brackets. Just change...

assert_not_nil flash(:notice)

to

assert_not_nil flash[:notice]

and I think we'll be fine. Try it again.

Paul's first Rails success!
Figure 20

Paul: Now that's more like it!

CB: Yep. You're definitely on a roll! How 'bout taking a look at the rest of the test case and the category controller and tell me what you see.

Paul: OK. Let me just start at the top. We've already covered the index method. But we haven't talked at all about this next section in the Category controller. I've got an inkling of what it does, but I don't understand how it does it. What throws me is that it doesn't have the same format as all the other methods. All the others are blocks that begin with def method_name and end with end. This is just standing there all by itself.

the verify filter
Figure 21

CB: My bad. I should have brought that up myself. Rails has a group of methods that are called before_ filters. They're a little like the validations we used in the models, but these work in the controllers. The reason they're called before_ filters is that they are examined before every method invocation. According to the official documentation (at api.rubyonrails.org), the verify method "is essentially a special kind of before_ filter." What this is telling Rails is that before the destroy, create, or update methods are called, it needs to examine the request parameters and verify that the request type is a POST. If an attempt is made to invoke any of these methods with any other request type, the request should be directed to the list method. As far as adding a specific test method for this, I could go either way. The way they stand right now, if it somehow got lost our existing tests would let us know because they're testing for the view that's used to render the method they're testing. On the other hand, with a specific test for this, if the verify did get lost, we'd have a specific test that might point us to the problem more quickly. I'll leave it up to you.

Paul: OK. I'll think about that. Moving on... the list method creates an instance variable we're not testing for: @category_pages. So we need to add that to the test_list method.

list and test_list methods
Figure 22

The test_show method contains a test we haven't covered yet: assert_assigns (:category).valid?. I don't understand that, but since it's an extra test rather than a missing test let's come back to it in a minute.

show and test_show methods
Figure 23

We've already covered the new and create methods so we're OK on those. The test_edit method has that same new assertion as the test_show method, so we'll come back to that too. The test_update method is only testing the success path so I'm thinking it needs the same treatment we gave test_create. And lastly, the test_destroy method uses the exception-trapping assertions in a way I haven't seen before so we'll need to talk about that before I know if we need anything more there. Did I miss anything?

CB: Not that jumps out at me. Good job! OK, you want to go over those assertions we haven't covered yet?

Paul: You bet. What's the .valid? method doing in assert_assigns (:category).valid?

CB: What that's doing is running the validations in our model on the data being pulled out of the database. For performance reasons, when the fixtures get loaded the data in them isn't being run through the our model validations. So it's possible that we're loading data for our tests via our fixtures that our app wouldn't allow. This assertion is an example of how we might tackle the problem of making sure the data we use in our fixtures continues to be the right stuff as we grow our app. Right now I don't see us needing to make much fuss about it since we're still real early in the process, but it's something I'd like you to keep in the back of your mind. We might need you to figure out a good strategy for this if we end up with a lot of fixtures.

Paul: OK. I hadn't thought of that but I can see how it might become a problem. I'll give it some thought. What about the assert_nothing_raised and assert_raised assertions in the test_destroy method? I know we just used assert_nothing_raised to trap a fatal exception in our Category Unit test. But why would the Rails scaffolding use it here? The find doesn't throw a fatal exception.

destroy and test_destroy methods
Figure 24

CB: Good question, Paul. And you're right, in Rails, an unsuccessful find throws an exception, but it's not a fatal one. You probably remember that in our Unit tests we invoked Category.find several times without checking for an Exception. We used assert_not_nil to test that a record was successfully retrieved, and assert_nil to test that a record was not successfully retrieved. Here, Rails' test scaffolding is doing the same tests a different way: by checking to see if a RecordNotFound Exception was raised. Even though a find doesn't really drive the need, I think there are a couple of good reasons to show this approach in the test_destroy method.

First, we know that Rails throws a fatal exception when we try to destroy an object that has children. So, for starters, if Rails is going to scaffold this approach in any of the test methods, this is a pretty good one. Second, we added the check_for_children method to our Category model to fix the problem in the app. If that weren't the case, and if the Category record had any child records in the Recipes table, this test would crash our test case. If we thought there was a risk that the check_for_children method might get lost, instead of POSTing a request to destroy the record, we could invoke Category.destroy inside one of these two assertions. Then the Exception will be trapped and the test method will just pass or fail, depending on the situation, instead of crashing our test case. I'm not suggesting we do that here, because we already have a Unit test, test_cannot_delete_record_with_child, that covers us. In fact, we used assert_nothing_raised in that test. I'm just reminding you that these assertions give us the ability to do it here too.

Paul: I'm ready to give it a shot. Here goes...

I think I want to go ahead and add a test method for the verify. I like the idea of having a specific test that could help decrease our debug time if something happened to it. So I'll add...

def test_verify_gets_are_safe
   get :destroy
   assert_redirected_to :action => 'list'
   get :create
   assert_redirected_to :action => 'list'
   get :update
   assert_redirected_to :action => 'list'  
end

For the test_list method, I need to add a new assertion to make sure we test for the creation of both instance variables the controller method is creating.

assert_not_nil assigns(:category_pages)

The test_show method is OK, and we've already covered the test methods for the new and create methods in the controller. The test_edit method is OK too. Which brings us to the test_update method.

update and test_update methods
Figure 25

The scaffolded method is only testing a successful update, just like the test_create method was doing before we fixed it. So I think we ought to make the same sort of change here.

CB: That's fine. But before you do, I want to make sure we're on the same page as to why we're doing what we're doing. In the test_create_success_and_failure method you followed the path that the Rails scaffolding had laid down for us; checking the number of records to verify a successful create and failed create. There's nothing wrong with that. But do we really need to do that test in our Functional tests?

Paul: I'm not following you. Why wouldn't we want to make sure the app actually did what it says it did?

CB: Our Unit tests are already testing to validate that the CRUD functionality is working.

Paul: Ahhh... I see what you're saying. So, the question is "do you really want to repeat yourself?" I think you're right. We don't need to do that test again here.

CB: I didn't say we don't. I asked. Let me ask it differently. Is there some value we might get if we did decide to repeat the test here in our Functional test? Say, for example, that we ran our Unit tests and they passed. And then we ran our Functional tests and this one failed. Would that tell us something of value?

Paul: Yeah. It would tell us that something bad had happened to our Unit tests.

CB: Exactly. So, if we think the probability of that happening is high, we might want to go ahead and duplicate the test, but more with the intent of testing our tests, and not so much to retest our app.

Paul: I see what you're saying. Thanks for making me think about it. In fact, I was thinking about doing pretty much the same thing we did in our Unit test; changing the data and then retrieving it to make sure it actually got changed in the database. But now that I think about it, maybe there's a better way to approach this. What do you think about this?

def test_update
   post :update, :id => @first_id
   assert_response :redirect
   assert_redirected_to :action => 'show', :id => @first_id
   post :update, :id => @first_id, :name => ''
   assert_template 'edit'
end

CB: That looks like a pretty good first stab at it. Let's run the test case and see what we get.

Paul: Good idea.

ruby test\functional\category_controller_test.rb

results of new test_update method
Figure 26

So, what do you think? You good to go?

Paul: Rendering with nil? What the heck? The controller method says that if the update isn't successful the app's supposed to render the edit template. I don't get it.

CB: Take a step back and think about what we're really testing here. Are we really testing the controller logic you're looking at? We're trying to test the path the app takes if the create is unsuccessful. What caused our create to fail? Our validations! And when a validation fails it generates error messages and displays them to the last page that was rendered. But the internal processes make it happen in a way that makes it a little tough to test for in the most obvious way. Let's fire up the app again and do the same thing our test method here is doing. Click the "Show all categories" link, click on a category name, then click edit. Then delete the name and click the button.

error in the app from trying to update with blank name
Figure 27

CB: See the URL? It still says it's in the update method. I'm guessing that explains the "rendering with <nil>" failure we got since there isn't any "update" template. But it's clear that Rails processed the request and redirected the visitor back to the edit page with error messages. So, let's change the line in your test_update method from

assert_template 'edit'

to

assert_response :redirect

And now rerun the test case...

testing for a Rails-generated error
Figure 28

Paul: OK! I think all that leaves is the test_destroy method. Let's take another look at that.

another look at the destroy and test_destroy method
Figure 29

Paul: You know, now that I understand the Exception handling assertions a little better, and I remember that we've got good protection both in our code and in our Unit tests to protect against crashes from trying to destroy records that shouldn't be destroyed, I don't think this needs any work. If the assert_nothing_raised fails on the find attempt, the test case will end with failure which will tell us we have a problem with our test data. If the find succeeds, attempting to destroy the record won't cause us a problem. And then we're checking to make sure the record actually was deleted from the table. That's a little redundant with our Unit tests in place, but it's not a problem.

CB: OK, then. You ready to tackle the recipe controller tests?

Paul: Absolutely. They're pretty much just going to be a clone of these at this point. Here's the category_controller_test.rb file as it stands right now.

our category_controller_test
Figure 30

CB: OK. That looks great. Let's give it one last run and make sure we're good here before we get to work on the recipe_controller_test.

ruby test\functional\category_controller_test.rb

final run of category_controller_test
Figure 31

Looking good! Looks like we're ready to move on to the category controller test. Let's go ahead and fix the reference to the :first fixture before we do. In the setup method, change...

@first_id = recipies(:first)

to

@first_id = recipes(:one)

And since we know that recipes need to have a title and to be assigned to a category for our validations to work, let's go ahead and change the line in the test_create method to avoid that error. Change...

post :create, :recipe => {}

to

post :create, :recipe => {:title => "new recipe", :category_id => 1}

And now let's give our recipe controller test a run to see where we are on that.

ruby test\functional\recipe_controller_test.rb

our first run of the recipe controller test
Figure 32

Paul: I'd say that's a pretty good start ;-)

CB: Gotta agree with you there, Paul ;-) You want to take us through the changes we need to make?

Paul: Sure. I'll just start at the top again. The test_index method looks OK. I'm going to copy over the test_verify_gets_are_safe method from the category controller test.

def test_verify_gets_are_safe
   get :destroy
   assert_redirected_to :action => 'list'
   get :create
   assert_redirected_to :action => 'list'
   get :update
   assert_redirected_to :action => 'list'  
end

We changed the list method in the recipe controller so it would filter for a category, though, so it's different from the list method in the category controller.

the list methods are different
Figure 33

I think we need to beef up our test method here. Here's what we've got now.

scaffolded test_list method for recipe_controller_test
Figure 34

Paul: This test doesn't pass in anything for the category_id param so that takes care of the first branch in the list method. I think we just need to do a second pass in here passing in the category_id and making sure the same assertions still pass. It doesn't make sense to me to try to make sure that only the right records were returned, especially not in the controller test. If we were going to test for that, I'd say we'd need to do it in the Unit tests. What do you think?

CB: Sounds good to me.

Paul: Cool. So, I'm going to replace the test_list method with one I'll name test_list_all_and_filtered.

def test_list_all_and_filtered
   get :list
   assert_response :success
   assert_template 'list'
   assert_not_nil assigns(:recipes)
   get :list, :category_id => '1'
   assert_response :success
   assert_template 'list'
   assert_not_nil assigns(:recipes)
end

The test_show and test_new methods are OK, but the test_create needs the same treatment we gave it in the category controller test. So I'll replace test_create with test_create_success_and_failure. pretty much just like we did before, except for the variable names. We just have to remember that recipes need both a title and the foreign key to save successfully.

def test_create_success_and_failure
   num_recipes = Recipe.count
   post :create, :recipe => {:title => "new recipe", :category_id => 1}
   assert_response :redirect
   assert_redirected_to :action => 'list'
   assert_not_nil flash[:notice]
   assert_equal num_recipes + 1, Recipe.count

   num_recipes = Recipe.count
   post :create, :recipe => {:title => ""}
   assert_response :success
   assert_template 'new'
   assert_equal num_recipes, Recipe.count
end

For the test_update method, we need to make sure invalid updates generate errors, just like we did in the category controller. So we'll add the same two lines to the end of the method.

post :update, :id => @first_id, :title => ''
assert :redirect

Which brings us to the test_destroy method which, on the same reasoning we came to in the category controller, seems to be OK for the moment.

CB: Excellent, Paul. Let's see whatcha got.

our completed recipe_controller_test
Figure 35

CB: Looks great! Let's run the test case and see what we get.

ruby
test\functional\recipe_controller_test.rb

our category controller test results
Figure 36

CB: Well now. I'd say that's a pretty good session's worth of work. What do you think, Paul?

Paul: As much as I'd like to jump in on the Integration tests, I do need to run. Got another meeting with some customers to review test plans.

CB: You know, I was thinking that maybe we should bring Boss into the session to work on the Integration tests. That's where I'm really hoping to engage with him, and it'd give you a good chance to see how this might work into the test planning model. What do you think?

Paul: I think that could be real interesting. Let me get with Boss and see when he can break free to join us. I'll do my best to bring him along next time. See ya, bud.

Articles in this series

Bill Walton is a software development/project management consultant/contractor.


Return to Ruby.

Copyright © 2009 O'Reilly Media, Inc.