advertisement

Print

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

Paul: So Rails is reporting that the test failed, but we expected it to fail didn't we? I'm not sure I'm following this.

CB: No problem. We didn't expect the test to fail. We expected the save to fail. Note that I'm using the word expect a little loosely here. In fact, we knew the test would fail because the test makes sure the system is working correctly and we already know that's not true. If the system were working correctly, Category.save would have failed which would have returned nil and the test would have passed. Our test proved that the system's not working correctly by reporting the failure. But because the database is assigning an empty string as the default value, the save is working even though it's not supposed to. We could change the schema, and later we will, so that the database doesn't assign that default value, but we'd still have the same basic problem with visitors creating blank entries by hitting the space bar a time or two for that field. Our tests are telling us we need to write some code. Let's start by adding a validation that checks to make sure something's been entered for the name. We need to add one line to our Category model. So we'll open app\models\category.rb and, right below our relationship definition, add one line:

validates_presence_of :name

And now let's see what our tests are telling us.

ruby test\unit\category_test.rb

a new failure
Figure 32

Paul: Well, I guess that didn't fix it. It's still failing.

CB: Look closer, Paul. That's not the same failure. But don't feel bad that you didn't pick that up right away. It's my bad that I haven't told you how to read the output yet. See the line right below "Started"? The "F" in the first position is telling us that the Failure was in the first test method that Rails executed, but that two more methods executed successfully after that. Each success is shown as a ".". The last time, our failure was in the second test method and the first and third methods executed successfully. Those indicators get posted while the test case is executed. After it's finished, the details of where the failure (or error) occurred and what happened get posted. So, in this case, we can see that, by adding the validation, we've introduced a failure in the test_create_and_destroy test method and the specific assertion that failed is at line 21. Let's take a look.

a new failure
Figure 33

CB: See where the failure message is telling us "<3> was expected but was <2>"? It's saying that the first argument in the assert_equal evaluated to "3" but the second argument evaluated to "2" and so the assertion failed. The reason it failed is that our new_rec record didn't get saved, and the reason it didn't get saved is that our new validation prevented it, because we tried to save the record without assigning a value to the name field. Doh ;-p So, all we need to do is give it a name before we try to save it. I'll add...

new_rec.name = "for validation"

just before the save and then run our category test case again.

ruby test\unit\category_test.rb

now working!
Figure 34

CB: Ta Da! ;-)

Paul: Not so fast, CB. I'll admit it's pretty cool that we caught a problem with our tests by running our tests. But we still need to make sure that we don't allow records to get saved if the visitors just hit the space bar a couple of times. And we've also got to make sure the system doesn't allow names that are too long either.

CB: I know, I know. I just love to celebrate the small wins too ;-) To test the space bar case, we'll just add a couple of lines to our test_must_have_a_valid_name:

rec_with_blank_name = Category.new(:name => '   ')
assert_equal(false, rec_with_blank_name.save)

And now we rerun out category test...

validation's not fooled by blanks
Figure 35

Paul: Well that's cool. It looks like validates_presence_of isn't fooled by a couple of blanks.  

CB: So now we just have to test for a name that's too long. I think I'll add a new test method for this because I'll need to do a little set up. And I think we'll want to test to make sure that we can save one that's exactly on the limit we've set, and not save one that's just over the limit. I'll add this to category_test.rb.

def test_long_names
  partial_name = ''
  'a'.upto('y') {|letter| partial_name << letter}
  rec_with_borderline_name = Category.new(:name => (partial_name * 4))
  assert_equal(100, rec_with_borderline_name.name.size)
  assert rec_with_borderline_name.save
  rec_with_too_long_name = Category.new(:name => ((partial_name * 4) << 'z'))
  assert_equal(101, rec_with_too_long_name.name.size)
  assert_equal(false, rec_with_too_long_name.save)
end

And now let's run it.

new run with length check
Figure 36

CB: And we've got a failing test, so it's time to write some code!

Paul: Before you do, CB, I've got a couple of questions. For starters, I notice that the line just under "Started" shows the "F" in the second position. But I just saw you add that new method at the end of the file. That output says the failure was close to the beginning. What's up with that?

CB: Good eye, Paul. The test methods don't necessarily run in the same order they appear in the test case file. That's why we need to look at the numbered explanations to see what line the assertion that failed is on. In this case, it's line 44, which is the last line of the new method, which is, in fact, the last method in our test case.

assert_equal(false, rec_with_too_long_name.save)

And what the failure is telling us is that the record got saved, even though the name is too long. What happens with strings that are too long in MySQL is that they just get truncated. That's not a very user-friendly way to handle things, but we'll fix that by adding another validation to our model. Open up app\models\category.rb and, right below validates_presence_of, add a new line:

validates_length_of :name, :maximum => 100

Paul: OK, but before I do, what about a minimum length? Do we want to allow names like 'X'?  

CB: That's a really good question, Paul. One I wish Boss were here to answer. Tell you what, let's use a different option on the validation that'll let us specify a range. We already know the name has to be at least one letter long so, technically, we won't really be doing work he hasn't asked for. So let's add...

validates_length_of :name, :in => 1..100

to app\models\category.rb instead. Then if Boss wants to change the minimum, we're ready for him. And after we add the new validation, we run our test case again...

ruby test\unit\category_test.rb

now checking length
Figure 37

Paul: Cool. But, before I forget, I had another question. When we want to check to make sure a Category record gets found, we use assert. When we want to check that it's not found we use assert_nil. When we want to check to make sure a Category record is successfully saved, we use assert again. But when we want to check to make sure it didn't get saved, we use assert_equal(false, ...). For me, that makes it harder to read the test. Can't we just use assert_false ?

CB: I like it! And of course we can. Except Rails doesn't include assert_false in its standard set of custom assertions. But, like I said earlier, we can create our own custom assertions.

Paul: How hard is it?

CB: Dude! This is Rails!!! By now you should be asking, "How easy is it?" ;-)

The test_helper.rb file gets automatically loaded for every test case. So any assertion method we define there can be used by any test case. So that's where we'll put ours.

def assert_false(record_being_saved)
   assert_equal(false, record_being_saved)
end

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

Next Pagearrow