advertisement

Print

Cookin' with Ruby on Rails - Designing for Testability
Pages: 1, 2, 3, 4, 5, 6, 7

schema.rb
Figure 21



There's the problem. See the :default => '' parameter on the name and title columns? We told the database the field couldn't be null, but an empty string is not null to the database. I don't remember why I did that. Probably just to hurry things along during that first session. I probably thought something like, "I'll just do this for now and come back and fix it later." Yet another great example of why we ought to be writing our tests first. To answer your question, though, we'll use Rails validation methods.

But before we start talking about validations, think about what you've seen so far. We generated a test database in two steps. Using the test stubs that Rails had already generated for us, we found out that Rails and the database were working properly. And right out of the box, our tests told us we'd missed some important things when we wrote the code. And now they're telling us what code we need to write to fill those holes. If we'd written the tests before we wrote the code, we wouldn't have those holes at all.

Paul: Well, I'm definitely impressed with how easy it was to get this far. And it's definitely obvious now that we've got some holes that would turn into problems if we didn't get them taken care of before we gave the app to customers to test. But, to tell you the truth, I haven't gotten my arms around what you mean by writing tests before we write code or how that would work.

CB: That's fair. Since we know now that we've got some code we need to write, what do you say we use that as an opportunity to explore the notion? Go ahead and delete that record we just created, and then let's go ahead and flush out our fixtures so we'll have a couple of good records in the database for every test method to use. I think I'll just copy the info off the show page from the records we've already created.

So categories.yml will have:

one:
  id: 1
  name: main course
two:
  id: 2
  name: beverages

And recipes.yml will contain

one:
  id: 1
  category_id: 1
  title: pizza
  description: CB's favorite lunch
  date: 2007-05-09
  instructions: Phone pizza joint. Place order. Pay delivery guy. Chow down!

two:
  id: 2
  category_id: 2
  title: iced tea
  date: 2007-05-09

CB: So now we've got a couple of records to work with. Let's write some tests! Let's start with our category model. Open up category_test.rb and we'll write some methods to test our app's basic CRUD functionality. Let's get rid of the test_truth method and replace it with one that tests the Category model's basic read and update functions.

def test_read_and_update
  rec_retrieved = Category.find_by_name("beverages")
  assert_not_nil rec_retrieved
  rec_retrieved.name = "beverage"
  assert rec_retrieved.save
end

Notice how we named our new method. It's important to remember that all Rails test methods have to begin with test_. So now let's rerun our category test case.

ruby test\unit\category_test.rb

category read and update test fails
Figure 22

Paul: Again? I thought we fixed this!

CB: Actually, this is a different error, Paul. See the third line of the error message? It says, "cannot delete or update ..." The error we had earlier said "cannot add or update." What's going on here is that when Rails encounters the test method, the first thing it does is delete all the rows in the table and then load the table from the fixtures. And since there's a recipe in the table for each category, MySQL isn't going to let us delete the category records.  

We've seen this problem before too. Remember towards the end of our first session when we tried to delete a category that had recipes? We got this same error (Figure 33). At that point, we just put a quick patch in place to check to see if there were any recipes before we attempted to destroy the category, and, if there were, then we just returned without doing anything. At that point we said we'd figure out how we really wanted to handle it later. Looks like later has arrived ;-)

Let's tackle the problem in two pieces. The first thing we need to do is decide how we want to avoid this problem during our testing. The way it stands right now we're not going to be able to load any category fixtures if there are records in the recipes table. The simplest way to solve this is to make sure that there aren't any records in the recipes table. When we run the recipes test, we need to clean out all the records in the table before we finish. Remember me mentioning the setup and teardown methods? They get run before and after each test method if they exist in the test case. Let's use the teardown method to fix this problem for our testing purposes. Open up recipe_test.rb and add:

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

Now we'll rerun recipe_test.rb

ruby test\unit\recipe_test.rb

rerunning recipe_test.rb
Figure 23

And now let's make sure that our new teardown method has cleared out the database.

recipes table after teardown
Figure 24

CB: Oops ;-) My bad. The good news is, we know the teardown code worked since the test case finished without errors. What's going on here is something we haven't talked about yet. Remember I said that Rails reloads the table from the fixtures before every test method? Well, there are two ways we can make that happen. One way is to let Rails actually delete all the records and then reload them. But that can burn a lot of cycles if we have a lot of methods and a lot of fixtures, so Rails gives us a higher-performance option: a transaction-based approach. By default, Rails will delete all the records and load from fixtures for the first test method, then use transactions to roll back the table to that state at the end of that method so the table's ready for the next one. After the next method completes, Rails rolls back the table again. And so on. The transaction-based approach is the default setting, and I forgot to change it. Let's open up test\test_helper.rb and change the line that says:

self.use_transactional_fixtures = true

to

self.use_transactional_fixtures = false

And now let's run the recipe test case again, and see if that fixed the problem.

rerunning recipe test case
Figure 25

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

Next Pagearrow