So far, we have dealt with the simple case where a controller manages create, update, and delete operations for a single instance of the Person
model at a time. However, this is only part of the story.
Each person also optionally has an associated address, stored in the addresses
table. We separated addresses from people, as companies also have addresses: it is good practice to store all instances of a certain type of data in a single table. In the case of Intranet, this makes searching for an address far easier, as we only have to search over a single table in the database. On the other hand, it makes managing addresses tougher, as we potentially have two controllers (PeopleController and CompaniesController)
that can create and modify Address
instances.
While this use case is not rare, it is difficult to find good examples of implementations in the wild. Hopefully, we will address (excuse the pun) this deficiency in the next section. We'll implement a single form, which will enable a user to both add a new person to the database and optionally, at the same time, add a new address.
To insert an address at the same time as we insert or edit a person's record, we need to add some new address fields at the bottom of app/views/people/_form.rhtml
. The user can use these fields to enter the address at the same time as they enter a person's details. As we're going to need these address fields when creating companies too, we'll put them into a partial, app/views/address/_form.rhtml
, so we can reuse them more easily:
<% if address.errors[:base] -%> <p><%= error_message_on :address, :base %></p> <% end -%> <p><%= label :address, 'Street 1', :required => true %><br /> <%= text_field :address, :street_1 %> <%= error_message_on :address, :street_1 %></p> <p><%= label :address, 'Street 2' %><br /> <%= text_field :address, :street_2 %></p> <p><%= label :address, 'Street 3' %><br /> <%= text_field :address, :street_3 %></p> <p><%= label :address, 'City' %><br /> <%= text_field :address, :city %></p> <p><%= label :address, 'County' %><br /> <%= text_field :address, :county %></p> <p><%= label :address, 'Post code', :required => true %><br /> <%= text_field :address, :post_code %> <%= error_message_on :address, :post_code %></p>
A few points to note about this form:
@address
instance variable when creating the form fields here, but use address
instead. We'll pass this address
argument in as a local variable (through the :locals
option) when we render the partial. text_field
helpers and specifying :address
as the first argument to each method call. This will yield a set of form elements with names in the format address[<field_name>]
, where<field_name>
is the name of a field in the addresses
table. label
helper we developed earlier in this chapter is used throughout. :base
, which is not a field associated with an Address
instance. In fact, this refers back to a generic error message we set if there is a validation error on the whole instance; namely, if street_1
and post_code
reference an existing address (see the section Validating Addresses in Chapter 4). This message is shown at the top of the form, as it applies to the whole address, and not just to a single field.Next, we pull this form into the main form for adding a new person (app/views/people/_form.rhtml):
...
<p><%= label :person, 'Notes' %><br />
<%= f.text_area :notes %></p>
<div id="address">
<h2>Enter address details (optional)</h2>
<%= render :partial => 'addresses/form',
:locals => {:address => @address} %>
</div>
<p><%= submit_tag 'Save' %></p>
The new lines are highlighted. These just render the new partial inside a<div>
element, passing a local :address
variable to the app/views/addresses/_form.rhtml
partial. At the moment, :address
references an instance variable, @address
, which we haven't set yet. Let's do that in the controller (app/controllers/people_controller.rb). While we're doing this, let's add some code to save a person's address and assign it to the person, too:
class PeopleController < ApplicationController # ... other actions ... def new @page_title = "Add a new person" @person = Person.new @address = Address.new end def create @person = Person.new(params[:person]) @person.build_address(params[:address]) if @person.save redirect_to_index 'Person added successfully' else @page_title = "Add a new person" @address = @person.address render :action => 'new' end end end
We're just creating a new Address
instance here, and building it from the :address
part of the request parameters (which gathers all the form fields whose names begin with "address" into a hash), using the build_address
method automatically added by the association (see the section Associations between Models in Chapter 4). As we're creating a new Person
, Rails will save the address to the database when we save the person associated with it.
This works perfectly well if both the person and address validate first time. However, we get problems if we want to add a person without an address. Our code won't let us do this, as it always tries to save the address, which sometimes we don't want to fill in (e.g. if we don't have someone's home address).
To fix this, we'll add a class method to the model, from_street_1_and_post_code
, which will give us an address, depending on whether the user has supplied the street_1
and post_code
parameters; in cases where they haven't supplied a street_1
or post_code
, it returns nil
. The method utilizes the Rails find_or_initialize_by_*
method to either retrieve an existing address or initialize a new one (without saving it). This method is similar to the find_by_*
methods discussed in Chapter 4 (in the section Finding Records Using Attribute-Based Finders), and can be passed multiple fields to initialize a record. There are also find_or_create_by_*
methods available to models, which will additionally save records they create. from_street_1_and_post_code
also updates the attributes of the retrieved or initialized object from a hash of parameters passed to the method:
class Address < ActiveRecord::Base # ... other methods ... # Look up or initialize an address from params # if street_1 or post_code supplied; # otherwise return nil; NB does not save the address. # # +params+ is a hash of name/value pairs used to set # the attributes of the Address instance. def self.from_street_1_and_post_code(params) params ||= {} street_1 = params[:street_1] post_code = params[:post_code] if street_1.blank? and post_code.blank? address = nil else address = find_or_initialize_by_street_1_and_post_code(street_1, post_code) address.attributes = params end address end end
We then modify the create
action to use this new model method:
class PeopleController < ApplicationController # ... other actions ... def create @person = Person.new(params[:person]) @person.address = Address.from_street_1_and_post_code(params[ :address]) @address = @person.address || Address.new if @person.save redirect_to_index 'Person added successfully' else @page_title = "Add a new person" render :action => 'new' end end end
If no street_1
or post_code
parameters are in the request, Address.from_street_1_and_post_code
returns nil
, and the person's address is set to nil
; however, we still need a valid Address
instance for use in the view. So, in the second highlighted block, we set the @address
instance variable to the person's address; or, if it is nil
, to a new Address
.
When @person.save
is called, the validity of the new person record is checked. Recall from Chapter 4 that a person's address is only valid if it is nil
or a valid Address
instance. If the person is valid and their address is valid or nil
, both will be saved; otherwise, neither is.
Try adding some new people to the application, with valid person and address details, with valid person details only, with valid address details only, and with invalid person and address details. You should only see validation error messages on the address if either street_1
or post_code
is set; otherwise, the person should be added without an address (providing the person fields validate).
The actions defined in the previous section are sufficiently complex to feel nervous about. We need to be sure that the controller responds correctly to different combinations of request parameters: street_1
set, but post_code
not; valid address, but invalid person; and so on. In Chapter 4, we saw how unit testing can be used to codify expectations about how models should validate. Functional testing can be used in a similar way for controllers, to store expectations about how they should work and ensure that those expectations aren't broken by changes to the code.
Functional tests effectively interact with the application in the same way that a client browser does, making requests to controller actions and receiving responses; the testing occurs on the responses, where we can check that the correct response codes were received, the response body contained the correct HTML, validation is managed correctly, the client was redirected correctly, and so on.
Each time you generate a controller, Rails adds a functional test skeleton for it to the test/functional
directory, with the name<controller name>_controller_test.rb
. To write functional tests for the PeopleController
class, for example, we need to modify test/functional/people_controller_test.rb
. Open this file and delete the test_truth
method (which is just a stub to demonstrate the format of testing methods).
We want to test expectations about how the PeopleController's create
action should respond to different request parameters, as outlined in the table below:
Person parameters |
Address parameters |
Expectation |
Test method to create |
---|---|---|---|
Invalid |
Any |
Person not created; address not created; form displayed again |
test_create_bad_person |
Valid |
No street_1 and no post_code |
Person created with nil address; redirected to index |
test_create_person_nil_address |
Valid |
post_code, but no street_1 |
Person not created; validation errors returned for the address; form displayed again |
test_create_bad_street_1 |
Valid |
street_1, but no post_code |
Person not created; validation errors returned for the address; form displayed again |
test_create_bad_post_code |
Valid |
Valid (street_1 and post_code both supplied) |
Person and address both created successfully; redirected to index |
test_create_person_address |
Note that we're mapping each expectation to be tested onto a separate test method. For functional testing, as for unit testing, the test methods are named test_*
, a special name which Ruby's testing framework uses to identify methods to include in the test suite. Aside from the requirement to be prefixed with test_
, you can give your methods any name you wish: here, the method name includes the name of the action being tested (create) and bad
to denote cases where we're testing against invalid request data.
Here's how we code the first test, test_create_bad_person
. Add the test inside the PeopleControllerTest
class definition in test/functional/people_controller_test.rb:
# Test the create action with bad request parameters def test_create_bad_person # Send a post request to the create action with no parameters post :create # The response should be rendered using the people/new template assert_template 'people/new' # The response should contain a div with class 'formError' assert_select 'div[class=formError]' end
Some points to note:
post
method used sends a POST request to the specified action on this controller. There is also a get
method to send a GET request. Both can be supplied with a parameters hash (see later in this section).assert_template
can check whether an action renders a particular template, specified relative to the views directory. Here we make sure that the PeopleController's new
template (which shows the form for adding a new person) is rendered.assert_select
is a very powerful method for checking the content of the response body. It can be supplied with very fine-grained selectors (similar to CSS or XPath selectors), which attempt to find a matching element in the response body.To run the functional tests, call the following from the command line:
$ rake test:functionals ... Started .. Finished in 0.103625 seconds. 2 tests, 3 assertions, 0 failures, 0 errors
Any failed tests or errors are reported, along with the details of the test where they occurred. Note that we've run two tests here, as the CompaniesController
also has a stub for its functional tests.
Next is test_create_person_nil_address
, to test that a POST request with valid person parameters, but no address parameters, correctly creates a person with a nil
address:
def test_create_person_nil_address # Clear out the people table Person.delete_all # Post new person details with blank address post :create, :person => { :first_name => 'Bob', :last_name => 'Parks', :email => '[email protected]', :gender => 'M' } # Check there is one person in the database person = Person.find(:first) assert_equal '[email protected]', person.email # Check their address is nil assert_equal nil, person.address # Check we get redirected to the index after creation assert_redirected_to :action => :index end
Here we're sending some data in the POST body by passing a hash of parameters to the post
method. Note that you have to mirror the structure of the request as it would arrive from the form, meaning that you have to nest all of the person
parameters inside a nested hash, keyed by :person
. Also note that we can use assert_redirected_to
to specify a route we expect the controller to redirect to; the parameters passed to this mirror those used with link_to
and url_for
. In this case, if a person's record is successfully created, we expect to be redirected back to the index
action on this controller.
Similarly, to test requests where partial (invalid) address parameters are supplied, we do:
def test_create_bad_street_1 # Post valid person details, but with empty value for street_1 post :create, :person => { :first_name => 'Bob', :last_name => 'Parks', :email => '[email protected]', :gender => 'M' }, :address => { :post_code => 'B15 1AU' } # Check the new form is shown again assert_template 'people/new' # Check we get validation errors for street_1 input element assert_select 'div[class=fieldWithErrors]' do assert_select 'input[id=address_street_1]' end end
Note that this follows a similar format to the previous test, but that we are also passing an :address
key in with the post
parameters, specifying an invalid address (missing a street_1
attribute). We then use assert_select
to test for the presence of a<div>
element with class
attribute equal to fieldWithErrors
; and nest a further assert_select
inside it, to check that the error<div>
is wrapping the<input>
element with id
attribute set to address_street_1
. This tests that the form is displayed again, with an error message next to the street_1
form field.
test_create_bad_post_code
is similar to the previous test, and is not listed here: see the source code for the complete listing.
Our final test, test_create_person_address
, checks that if valid person and address data are supplied, both a person and an address are created and correctly associated:
def test_create_person_address # Clear out the people and addresses tables Person.delete_all Address.delete_all # Send post with valid person and address post :create, :person => { :first_name => 'Bob', :last_name => 'Parks', :email => '[email protected]', :gender => 'M' }, :address => { :street_1 => '11 Harley Street', :post_code => 'B15 1AU' } # Check person created person = Person.find(:first) assert_equal '[email protected]', person.email # Check address created address = Address.find(:first) assert_equal 'B15 1AU', address.post_code # Check person's address is the created address assert_equal person.address_id, address.id # Check redirected to index assert_redirected_to :action => :index end
This test uses many of the same methods as previous tests. The main things we're testing here are that the person and address are created, and that the address_id
attribute for the person is set to the id
of the newly-created address (highlighted).
This section has been a whirlwind tour of the potential of functional testing, and we've only skated over the surface of its possibilities. As you can see, it can be time-consuming to test every possible combination of request parameters; however, where you are writing mission-critical software, or software where the controller logic is complex, functional testing is a vital technique for ensuring consistency and stability in your application.
Luckily for us (thanks to Rails), the code for editing an existing person and their address is virtually identical to the code for creating a person. The chief difference is that the edit
and update
actions retrieve an existing person and their address (if available):
class PeopleController < ApplicationController before_filter :get_person, :only => [:show, :update, :edit, :confirm, :delete] verify :method => :post, :only => [:create, :update, :delete], :redirect_to => {:action => :index} # ... other methods ... def edit @page_title = 'Editing ' + @person.full_name @address = @person.address || Address.new end def update @person.address = Address.from_street_1_and_post_code(params[:address]) @address = @person.address || Address.new if @person.update_attributes(params[:person]) redirect_to_index 'Person updated successfully' else @page_title = 'Editing ' + @person.full_name render :action => 'edit' end end end
The only minor issue with this code is that an edit to someone's address will actually add a new address to the database, rather than update the old one. The old one will remain until manually removed from the system. However, it does mean that if someone enters an address that already exists in the database, that address is used instead of creating a new one. There is also the potential for creation of duplicate or near-duplicate addresses, if there are slight differences between how street_1
and post_code
are typed.
Finally, we need an app/views/people/edit.rhtml
template to present a form for editing a person, which pulls in the existing _form.rhtml
partial we created earlier:
<% form_for :person, @person, :url => {:action => 'update', :id => @person.id} do |f| %> <%= render :partial => 'form', :locals => {:f => f} %> <% end %>
3.149.234.188