Multi-Object Validation with Rails

so we’ll be good TDD citizens and the first thing we’ll do is to get a functional test that will fail. All of my functionality is in my `StoreController`, so the following test is in my `store_controller_test.rb` file:

def test_error_displayed_on_incomplete
post :create_order,
:customer => {:first_name => ””,
:last_name => ””, :email => ””}
end

assert_tag :tag => "div",    :attributes => {:id => "errorExplanation"}

This will `post` the empty string values to the controller, and then tests that we have a div with an id of “errorExplanation” in the resultant rendering.

A pattern has appeared through following this example. My view for this stage of the process is called `RegisterDetails` and has a corresponding `register_details` method which is part of `StoreController`. However, when registering, the form input is taken to the `create_order` method on the controller. Conceptually, there are 2 stages performed through the 2 separate methods:


  • Rendering

  • Processing

`register_details` renders the initial form, along with any errors and other messages, `create_order` actually performs the databases persistence upon successful filling of the form.

In Rails, validation is added to the model types, by adding various validator methods to the class. A simplification of my customer looks as follows

class Customer < ActiveRecord::Base  validates_presence_of :first_name, :last_name, :email    ...end

In this instance we’re checking that both name and email have been set and are not empty. If you run the tests, you should see them fail as we won’t have any error explanation div on the page.

To add validation error messages we can use the `error_messages_for` helper method, by putting the following in our `register_details` view (in this case, the `register_details.rhtml` file:

<%= error_messages_for :customer %>

This internally checks to determine whether the object (in this case `:customer`) has any items within it’s `errors` collection. If you were to run the test again now you’ll get a different error—because we’ve not put anything in `register_details` that can provide a customer for the view to render. It was this bit that had me stumped for a while, but makes sense now.

As I mentioned previously, Rails only performs validation when saving/creating/updating records—that is when there’s some kind of database persistence involved. We only need to save anything to the database is the data they post to `create_order` is valid, but we also need a `customer` accesible from `register_details`, surely that points to saving a `customer` inside `register_details`?

The answer is actually no. We create a `customer` inside `register_details`, but purely for the purposes of them answering the form—it’ll get thrown away after the rendering. So, to prevent the error from appearing we add the following to our controller:

def register_details  @customer = Customer.newend

We’ll need to add something to our `create_order` method in the controller to ensure we check the data before deciding what we do afterwards. If the user has answered incorrectly, we want them to see the registration form again, but this time, with the error message. If successful, we want to redirect.

So, continuing with the test to see the error message, we need to add the following code to pass the test:

def create_order  @order = get_order  @customer = Customer.new(params[:customer])  @order.customer = @customerend

if @order.save && @customer.save    # we'll add some stuff to process good orders soonelse    render :action => 'register_details'end

In my example, when checking out we create both an `order` and a `customer`—the former holding any order line items, and the latter the details about the customer.

For validation to work correctly in this kind of situation (where we’re validating multiple objects) we need to explicitly save both the order and customer. If we can’t save the items because of a problem with validation, we’ll then render the `register_details` view again. However, when we render it in this instance, the `@customer` variable that previously just held an ‘empty’ in-memory customer, now holds a `customer` that was created from the form. As a result, this `customer` object contains errors (that were added during the attempted save) and thus displayed as a message.

The final test is to make sure that if we do complete succesfully, we’re sent off to the `checkout` view. So, we write the following:

def test_redirected_to_checkout_when_successfully_registered  post :create_order,  :customer => {    :first_name => "Paul",    :last_name => "Ingles",    :email => "paul@here.com"     }end

assert_redirected_to :controller => "store", :action => "checkout"

and to pass we can update our `create_order` method to do the following:

if @order.save && @customer.save  update_order_from_cart  redirect_to :action => 'checkout'else  render :action => 'register_details'end

Finally, done!

I’m sure there’s more goodies tucked away in Rails’ validation helpers, but I’m not sure it’s entirely clear that saving a parent does not save it’s child (in my example, `Order.save` does not implicitly call `Customer.save` even though `Order` includes a `has_one :customer` declaration. Maybe I just missed something?

Anyway, at least I can finally go to sleep happy in the knowledge this iteration is going a little better than I thought it was earlier tonight!