Quest for the Holy Rails

May 10, 2007

The Games People Play (Part 1)

Filed under: pente, rails — Jake Brownson @ 6:31 pm

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.

Advertisements

Leave a Comment »

No comments yet.

RSS feed for comments on this post. TrackBack URI

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

Create a free website or blog at WordPress.com.

%d bloggers like this: