• 沒有找到結果。

Iteration B1: Validating!

在文檔中 Prepared exclusively for Jared Rosoff (頁 103-108)

Task B: Validation and Unit Testing

7.1 Iteration B1: Validating!

While playing with the results of iteration A1, our client noticed something. If she entered an invalid price or forgot to set up a product description, the appli-cation happily accepted the form and added a line to the database. Although a missing description is embarrassing, a price of $0.00 actually costs her money, so she asked that we add validation to the application. No product should be allowed in the database if it has an empty title or description field, an invalid URL for the image, or an invalid price.

So, where do we put the validation? The model layer is the gatekeeper between the world of code and the database. Nothing to do with our application comes out of the database or gets stored into the database that doesn’t first go through the model. This makes models an ideal place to put validations; it doesn’t matter whether the data comes from a form or from some program-matic manipulation in our application. If a model checks it before writing to the database, then the database will be protected from bad data.

Let’s look again at the source code of the model class (inapp/models/product.rb):

class Product < ActiveRecord::Base end

Prepared exclusively for Jared Rosoff

ITERATIONB1: VALIDATING! 104

Adding our validation should be fairly clean. Let’s start by validating that the text fields all contain something before a row is written to the database. We do this by adding some code to the existing model:

validates :title, :description, :image_url, :presence => true

Thevalidatesmethod is the standard Rails validator. It will check one or more model fields against one or more conditions.

:presence => true tells the validator to check that each of the named fields is present and its contents are not empty. In Figure7.1, on the next page, we can see what happens if we try to submit a new product with none of the fields filled in. It’s pretty impressive: the fields with errors are highlighted, and the errors are summarized in a nice list at the top of the form. That’s not bad for one line of code. You might also have noticed that after editing and saving the product.rb file you didn’t have to restart the application to test your changes—the same reloading that caused Rails to notice the ear-lier change to our schema also means it will always use the latest version of our code.

We’d also like to validate that the price is a valid, positive number. We’ll use the delightfully named numericality option to verify that the price is a valid num-ber. We also pass the rather verbosely named:greater_than_or_equal_tooption a value of0.01:

validates :price, :numericality => {:greater_than_or_equal_to => 0.01}

Now, if we add a product with an invalid price, the appropriate message will appear, as shown in Figure7.2, on page106.

Why test against 1 cent, rather than zero? Well, it’s possible to enter a number such as 0.001 into this field. Because the database stores just two digits after the decimal point, this would end up being zero in the database, even though it would pass the validation if we compared against zero. Checking that the number is at least 1 cent ensures only correct values end up being stored.

We have two more items to validate. First, we want to make sure that each product has a unique title. One more line in theProductmodel will do this. The uniqueness validation will perform a simple check to ensure that no other row in theproductstable has the same title as the row we’re about to save:

validates :title, :uniqueness => true

Lastly, we need to validate that the URL entered for the image is valid. We’ll do this using theformat option, which matches a field against a regular

expres-sion. For now we’ll just check that the URL ends with one of .gif, .jpg, or .png: regular expression

֒→ page68

validates :image_url, :format => { :with => %r{\.(gif|jpg|png)$}i,

:message => 'must be a URL for GIF, JPG or PNG image.'

ITERATIONB1: VALIDATING! 105

Figure 7.1: Validating that fields are present

}

Later, we’d probably want to change this form to let the user select from a list of available images, but we’d still want to keep the validation to prevent malicious folks from submitting bad data directly.

So, in a couple of minutes we’ve added validations that check the following:

• The field’s title, description, and image URL are not empty.

• The price is a valid number not less than $0.01.

• The title is unique among all products.

• The image URL looks reasonable.

Report erratum this copy is (B8.0 printing, September 9, 2010)

Prepared exclusively for Jared Rosoff

ITERATIONB1: VALIDATING! 106

Figure 7.2: The price fails validation.

Your updatedProductmodel should look like this:

Download depot_b/app/models/product.rb

class Product < ActiveRecord::Base

validates :title, :description, :image_url, :presence => true

validates :price, :numericality => {:greater_than_or_equal_to => 0.01}

validates :title, :uniqueness => true validates :image_url, :format => {

:with => %r{\.(gif|jpg|png)$}i,

:message => 'must be a URL for GIF, JPG or PNG image.' }

end

ITERATIONB1: VALIDATING! 107

Nearing the end of this cycle, we ask our customer to play with the application, and she’s a lot happier. It took only a few minutes, but the simple act of adding validation has made the product maintenance pages seem a lot more solid.

Before we move on, we once again try our tests:

rake test

Uh oh. This time we see failures. Two, actually. One intest_should_create_product and one intest_should_update_product. Clearly something we did caused some-thing to do with the creation and updating of products to fail. This isn’t all that surprising. After all, when you think about it, isn’t that the whole point of validation?

The solution is to provide valid test data intest/functional/products_controller_test.rb:

Download depot_c/test/functional/products_controller_test.rb

:title => 'Lorem Ipsum' , :description => 'Wibbles are fun!' , :image_url => 'lorem.jpg' , :price => 19.95 }

end

test "should get index" do get :index

assert_response :success

assert_not_nil assigns(:products) end

test "should get new" do get :new

assert_response :success end

test "should create product" do

assert_difference('Product.count' ) do post :create, :product => @update end

assert_redirected_to product_path(assigns(:product)) end

# ...

test "should update product" do

put :update, :id => @product.to_param, :product => @update

Report erratum this copy is (B8.0 printing, September 9, 2010)

Prepared exclusively for Jared Rosoff

ITERATIONB2: UNITTESTING OFMODELS 108

assert_redirected_to product_path(assigns(:product)) end

# ...

end

After making this change, we rerun the tests, and they report that all is well.

But all that means is that we didn’t break anything. We need to do more than that. We need to make sure that our validation code that we just added not only works now, but will continue to work as we make further changes. We’ll cover functional tests in more detail in Section 8.4, Iteration C4: Functional Testing of Controllers, on page124. As for now, it is time for us to write some unit tests.

在文檔中 Prepared exclusively for Jared Rosoff (頁 103-108)