In the last post we implemented a welcome controller that lets us sign up, in and out. Now we’re going to add a RESTful games resource to the project so users can manage games.
First let’s use the generator to give us a resource scaffold:
jake@jake-laptop:~/src/pente$ script/generate scaffold_resource game
exists app/models/
exists app/controllers/
exists app/helpers/
create app/views/games
exists test/functional/
exists test/unit/
create app/views/games/index.rhtml
create app/views/games/show.rhtml
create app/views/games/new.rhtml
create app/views/games/edit.rhtml
create app/views/layouts/games.rhtml
create public/stylesheets/scaffold.css
create app/models/game.rb
create app/controllers/games_controller.rb
create test/functional/games_controller_test.rb
create app/helpers/games_helper.rb
create test/unit/game_test.rb
create test/fixtures/games.yml
exists db/migrate
create db/migrate/002_create_games.rb
route map.resources :games
jake@jake-laptop:~/src/pente$
Now let’s define the columns for the games table in edit db/migrate/002_create_games.rb:
class CreateGames < ActiveRecord::Migration
def self.up
create_table :games do |t|
t.column :black_player_id, :integer
t.column :white_player_id, :integer
t.column :current_player_id, :integer
t.column :winning_player_id, :integer
t.column :created_at, :datetime
t.column :updated_at, :datetime
t.column :ended_at, :datetime
end
end
def self.down
drop_table :games
end
end
And migrate:
jake@jake-laptop:~/src/pente$ rake db:migrate (in /home/jake/src/pente) == CreateGames: migrating ===================================================== -- create_table(:games) -> 0.0163s == CreateGames: migrated (0.0165s) ============================================ jake@jake-laptop:~/src/pente$
Let’s add in a couple fixtures to work with in our tests:
one: id: 1 black_player_id: 1 white_player_id: 2 current_player_id: 1 created_at: <%= 1.days.ago.to_s :db %> updated_at: <%= 12.hours.ago.to_s :db %> two: id: 2 black_player_id: 2 white_player_id: 1 winning_player_id: 2 created_at: <%= 5.days.ago.to_s :db %> updated_at: <%= 2.days.ago.to_s :db %> ended_at: <%= 2.days.ago.to_s :db %>
Let’s associate it with players first defining some tests in test/unit/game_test.rb:
require File.dirname(__FILE__) + '/../test_helper'
class GameTest < Test::Unit::TestCase
fixtures :games, :players
def test_should_associate_with_players
assert_equal 1, games(:one).black_player.id
assert_equal 2, games(:one).white_player.id
assert_equal 1, games(:one).current_player.id
assert_equal 2, games(:two).winning_player.id
end
end
Your tests will now fail so let’s add some code to app/models/game.rb:
class Game < ActiveRecord::Base belongs_to :black_player, :class_name => 'Player', :foreign_key => :black_player_id belongs_to :white_player, :class_name => 'Player', :foreign_key => :white_player_id belongs_to :current_player, :class_name => 'Player', :foreign_key => :current_player_id belongs_to :winning_player, :class_name => 'Player', :foreign_key => :winning_player_id end
Let’s add associations to the player side starting with the tests in test/unit/player_test.rb:
... snip ...
fixtures :players, :games
... snip ...
def test_should_associate_with_games
assert players(:aaron).black_games
assert players(:aaron).white_games
assert players(:quentin).current_games
assert players(:aaron).won_games
end
... snip ...
And to make it pass by adding to app/models/player.rb:
... snip ... has_many :black_games, :class_name => 'Game', :foreign_key => :black_player_id has_many :white_games, :class_name => 'Game', :foreign_key => :white_player_id has_many :current_games, :class_name => 'Game', :foreign_key => :current_player_id has_many :won_games, :class_name => 'Game', :foreign_key => :winning_player_id ... snip ...
Now I’m going to go through and add several tests and show the code required to make it pass.
test/unit/game_test.rb:
... snip ...
def test_should_validate_presence_of_players
g = games(:one)
g.black_player_id = nil
assert !g.valid?
assert g.errors.on(:black_player_id)
g = games(:one)
g.white_player_id = nil
assert !g.valid?
end
... snip ...
app/model/game.rb
... snip ... validates_presence_of :black_player_id, :white_player_id ... snip ...
When you run your tests now the test we just wrote is passing, but we’ve broken another one! The generated functional test case contains a test that checks create. When we add validations to the model that won’t allow a model to be created with all of its attributes set to nil that test will break. We’ve got to modify the test to create a valid player.
test/functional/games_controller_test.rb:
... snip ...
def test_should_create_game
old_count = Game.count
post :create, :game => { :black_player_id => 1, :white_player_id => 2 }
assert_equal old_count+1, Game.count
assert_redirected_to game_path(assigns(:game))
end
... snip ...
Back to creating more tests:
test/unit/game_test.rb:
... snip ...
def test_should_validate_players_different
g = games(:one)
g.black_player_id = g.white_player_id
assert !g.valid?
assert g.errors.on_base
end
... snip ...
app/model/game.rb
... snip ...
validate :players_different
... snip ...
protected
def players_different
errors.add_to_base('Players must be different') if self.black_player_id == self.white_player_id
end
... snip ...
test/unit/game_test.rb:
... snip ...
def test_should_make_white_current_on_create
g = Game.create(:white_player_id => 1, :black_player_id => 2)
assert_equal g.white_player_id, g.current_player_id
end
... snip ...
app/model/game.rb
... snip ...
before_create :make_white_player_current
... snip ...
protected
def make_white_player_current
self.current_player_id = self.white_player_id
end
... snip ...
test/unit/game_test.rb:
... snip ...
def test_should_declare_winner
g = games(:one)
before = Time.now
g.declare_winner!(g.black_player_id)
assert_equal g.black_player_id, g.winning_player_id
assert_nil g.current_player_id
assert g.ended_at.between?(before, Time.now)
assert g.save
end
def test_should_raise_on_invalid_winner
g = games(:one)
assert_raise(Game::InvalidWinner) { g.declare_winner!(999) }
end
... snip ...
app/model/game.rb
... snip ...
def declare_winner!(winner_id)
raise InvalidWinner unless
[self.black_player_id, self.white_player_id].include?(winner_id)
self.winning_player_id = winner_id
self.current_player_id = nil
self.ended_at = Time.now
end
... snip ...
These tests will already pass because this is done automatically, but let’s put them in anyway:
test/unit/game_test.rb:
... snip ...
def test_should_set_created_at
before = Time.now
g = Game.create(:black_player_id => 1, :white_player_id => 2)
assert g.created_at.between?(before, Time.now)
end
def test_should_set_updated_at
before = Time.now
g = games(:one)
g.save
assert g.updated_at.between?(before, Time.now)
end
... snip ...
So we’ve written a pretty complete Game model now and have a full test suite for it! Isn’t TDD nice? I made it look easy by copy/pasting the working tests and code, but there was some finageling on my part to get things to work. It’s better to iron out all the problems now though rather than attempting to use the class and discovering problems while I’m trying to write other code.
In the next part we’ll write the controller and views to handle the new Game model.