rails --database sqlite3 sandbox
script/generate scaffold_resource Artist name:string
script/generate scaffold_resource Song title:string artist_id:integer
… and see if we have a green build.
rake db:migrate
rake
So far so good, including the 14 functional tests that came with the scaffold.
We want Song
to be associated to Artist
as a one-to-many relationship and we want our application to reflect that in a RESTful way, so that we can access an artist’s songs as http://expample.com/artists/11/songs/4
. In config/routes.rb
, we replace the two lines:
map.resources :songs
map.resources :artists
with
map.resources :artists do |artists|
artists.resources :songs
end
We need to tell Song
that it belongs_to :artist
and Artist
that it has_many :songs
. The SongsController
needs to be aware of the Artist associated with the songs:
class SongsController < ApplicationController
before_filter(:capture_artist)
def index
@songs = @artist.songs
respond_to do |format| format.html end
end
[...]
def create
@song = @artist.songs.create(params[:song])
respond_to do |format|
if @song.valid?
flash[:notice] = 'Song was successfully created.'
format.html { redirect_to song_url(@artist, @song) }
else
format.html { render :action => "new" }
end
end
end
private
def capture_artist
@artist = Artist.find(params[:artist_id])
end
end
The song_url
methods in the SongsController
and the Views in app/views/songs
also need to be updated to reflect the relationship between the Artist
and Song
resources, as song_url(@artist, @song)
, etc.
By now it would make perfect sense to delete the following two lines from config/routes.rb
, as they don’t any more make much sense as far as the context of our resources goes:
map.connect ':controller/:action/:id.:format'
map.connect ':controller/:action/:id'
At this point we have managed to break the build, because the tests in SongsControllerTest
do not match the newly defined routes. Testing the new routing map is relatively easy and can be achieved as:
def test_should_route_songs_of_artist
options = {:controller => 'songs', :action => 'index', :artist_id => "2"}
assert_routing('artists/2/songs', options)
end
Then, the Controller tests must conform to the amended routes.
def test_should_get_new
get :new, :artist_id => 1
assert_response :success
end
My complete test/functional/songs_controller_test.rb
looks like:
require File.dirname(__FILE__) + '/../test_helper'
require 'songs_controller'
class SongsController; def rescue_action(e) raise e end; end
class SongsControllerTest < Test::Unit::TestCase
def setup
@controller = SongsController.new
@request = ActionController::TestRequest.new
@response = ActionController::TestResponse.new
Artist.create(:name => 'test')
end
def test_should_route_songs_of_artist
options = { :controller => 'songs',
:action => 'index',
:artist_id => '2' }
assert_routing('artists/2/songs', options)
end
def test_should_route_song_of_artist
options = { :controller => 'songs',
:action => 'show',
:artist_id => '2',
:id => '1' }
assert_routing('artists/2/songs/1', options)
end
def test_should_get_new
get :new, :artist_id => 1
assert_response :success
end
def test_should_show_song
Song.create(:title => 'test')
get :show, :artist_id => 1, :id => 1
assert_response :success
end
def test_should_get_edit
Song.create(:title => 'test')
get :edit, :artist_id => 1, :id => 1
assert_response :success
end
def test_should_destroy_song
Song.create(:title => 'test')
delete :destroy, :artist_id => 1, :id => 1
assert_equal 0, Song.count
end
def test_should_redirect_to_songs_list_after_destroy
Song.create(:title => 'test')
delete :destroy, :artist_id => 1, :id => 1
assert_redirected_to songs_path
end
def test_should_update_song
Song.create(:title => 'test')
put :update, :artist_id => 1, :id => 1, :song => { }
assert_redirected_to song_path(assigns(:artist), assigns(:song))
end
def test_should_create_song
post :create, :artist_id => '1', :song => { }
assert_equal 1, Song.count
end
def test_should_show_song_after_create
post :create, :artist_id => '1', :song => { }
assert_redirected_to song_path(assigns(:artist), assigns(:song))
end
def teardown
Artist.find(:all).each do |a| a.destroy end
Song.find(:all).each do |s| s.destroy end
end
end
As the code stands now, and if you have removed all traces of fixtures from your tests, like I have (I usually avoid Rails fixtures, but that’s another story…), the build should be happy again.