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

Paul: Hmm... looks like something went wrong...

CB: Actually, something went exactly right. Let me walk you through what happened with both of these "bare bones" test cases. Let's start with a quick look at our test database. Here's our categories table.

category table
Figure 14

And here's our recipes table.

recipe table
Figure 15

CB: You can see that the categories table has two records in it. The records are empty except for having an id, but that's because the fixtures that we told Rails to use don't specify anything but the id field. When it executed the test case Rails found the line

fixtures :categories

That tells Rails to load the categories fixture (categories.yml) before executing each test method. In order to make sure that test methods don't "step on" each other, Rails resets the database to the state specified by our fixtures before running each test method. It deletes all the records in the table, and then reloads the table based on the fixtures we've told it to use. When Rails finished running the category test case it reported:

1 tests, 1 assertion, 0 failures, 0 errors

That's telling us that when it ran the test case (category_test.rb), Rails found one test method (test_truth) which contained one assertion (assert true) which produced the result we told Rails to expect (0 failures) and it didn't encounter any problems along the way (0 errors).

Paul: So assertions are how we compare the results we actually got with what we expected?

CB: Exactly. And Rails provides us with a whole set of custom assertions to make the comparisons easy to do. The standard set Rails provides includes things like like assert_equal, assert_nil, assert_not_nil, and so on. Plus, we can create our own. I'm not going to go into them all right now, but before you leave I'll give you a reading list so you can dig into that and more. In general though, assertions take two general forms. The first, most general, form is:

assert  expression

This lets us write assertions like:

assert Recipe.count <= 3

We can also use custom assertions. Rails provides some primitive ones, like assert_equal, and we can also define our own in the test_helper.rb file. Custom assertions have the form:

assert_some_condition(expected_value, thing_to_test_for_that_value)

Paul: So, fixtures are how we load data into the database, and the database gets reset before every test method execution. Is that right?

CB: Yep. There's also another way to prep the environment prior to the execution of test methods. If there's a setup method defined in the test case, category_test.rb for example, Rails will execute it prior to every test method. It's Ruby and Rails code, as opposed to the data in fixtures. Setup can be really helpful in cases where we need to set up data for specific test cases. And there's a related teardown method that gets executed after every test method, if it exists. We'll get into both of those later on.

Paul: And assertions are how we test to see if specific objects have the values we expect. OK. Let's move on to the recipe test. What happened with that? It looked like something went wrong to me, but you said it meant things went right. Want to explain that?

CB: You bet. Let's take another look at the recipes table.

object view of recipes table
Figure 16

And now look at the recipes fixture again.

recipes fixture without foreign key
Figure 17

Now look at the error message again. See the fourth line of the error message? The line that starts with "a foreign key constraint fails." Notice anything missing from the fixture?

Paul: Yeah. There isn't any value defined for the foreign key, category_id.

CB: Good eye. So let's add a category_id for each fixture. I like to keep the numbers lined up for readability, but you need to be a little careful doing it. This is a YAML file, and YAML doesn't allow tabs. So you either have to use the spacebar to do this, or make sure your text editor is set up so that hitting the tab key inserts spaces. Otherwise you'll get a parsing error.

update recipes fixtures
Figure 18

And now let's save our changes and rerun the test.

recipe_test results with new fixtures
Figure 19

Paul: So, if I understand this right, MySQL enforced the assignment of the foreign key that we have defined in our schema. Is that right?

CB: Exactly right. And that's why running the test before we'd added the foreign keys to our fixtures resulted in Rails telling us that had encountered an error, not a failure. Rails reports a failure when an assertion fails; that is, when we don't get the value we expected. Errors are things like this, where the system, rather than the application, is misbehaving, or when we have syntax errors in our code. That sort of thing. What happened a minute ago is basically that Rails told us, "MySQL says you're doing something wrong."

Paul: OK. But now that I think about it, our schema also says that categories have to have names and recipes have to have titles and that both names and titles can only be so long. MySQL is obviously treating those differently than the foreign key constraints. How do we test that those rules are getting enforced?

CB: I'm glad you asked ;-). Because the question underlines why I want to see us move to a test-first model of development. Think about it. The way it sits today, if we put this app in the hands of customers, they'd already be writing bug reports. Fire up Mongrel and our app again and you'll see what I mean. Create a new category but don't give it a name. Just click the Create button. See what happens?

create category with no name
Figure 20

Paul: I'll be darned. And I'll bet it would let the same thing happen with recipes too. Right? Oh yeah. I can see the bug list growing right in front of my eyes. So if telling the database not to allow it isn't enough, how do we make sure those rules get enforced?

CB: You know, the first thing we ought to do is figure out why the database is allowing it. Let's take a look at the schema. It's over in our db directory.

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

Next Pagearrow