やりクサし

先週の朝練の続きを云々することに。validate がなんとかなあたりなので正直かったるい。前回からの続き、ということで以下は実行しているらしい。

$ bundle exec rake db:test:prepare

一応 db 配下には test.sqlite3 が作成されてますね。

最初の例

として例示されているのが以下。

class User < ActiveRecord::Base 
  attr_accessible :name, :email

  validates :name, :presence => true
end

引数を括弧で、な例も示されてますが、わし的にはこっちの方が楽かも。あまり本文読まずに進めますが spec/models/user_spec.rb が以下らしい。

require 'spec_helper'

describe User do

  before(:each) do
    @attr = { :name => "Example User", :email => "user@example.com" }
  end

  it "should create a new instance given valid attributes" do
    User.create!(@attr)
  end

  it "should require a name"
end

ここで rspec spec/ してみれば良いのかな。

$ bundle exec rspec spec/
................*

Pending:
  User should require a name
    # Not Yet Implemented
    # ./spec/models/user_spec.rb:24

Finished in 1.83 seconds
17 examples, 0 failures, 1 pending
$

確かに pending と言われますな。続きを盛り込んでみます。
末端部分を以下に。

  it "should require a name" do
    no_name_user = User.new(@attr.merge(:name => ""))
    no_name_user.should_not be_valid
  end

もっかい試験を起動。

$ bundle exec rspec spec/
.................

Finished in 0.76727 seconds
17 examples, 0 failures
$

素晴しい。というか rubylisp/scheme 風な記述ができるあたりが好きなんですよね。で、email な属性についても以下な試験を追加。

  it "should require an email address" do
    no_email_user = User.new(@attr.merge(:email => ""))
    no_email_user.should_not be_valid
  end

これ、試験実行を emacs 上でできんのかな。って試験失敗しとるぞ。

$ bundle exec rspec spec/
.................F

Failures:

  1) User should require an email address
     Failure/Error: no_email_user.should_not be_valid
       expected valid? to return false, got true
     # ./spec/models/user_spec.rb:31:in `block (2 levels) in <top (required)>'

Finished in 4.4 seconds
18 examples, 1 failure

Failed examples:

rspec ./spec/models/user_spec.rb:29 # User should require an email address
$

あ、これは model の記述がアレなのか。以下を app/models/user.rb に追加。

  validates :email, :presence => true

で再度試験実行。

$ bundle exec rspec spec/
..................

Finished in 0.67333 seconds
18 examples, 0 failures
$

この重さはやっぱ例のツールを使いたくなりますな。とりあえず試験はパス。ここで _the “presence” validations are complete_ とのこと。

6.2.2 Length validation

どんどん進めます。ええと、以下を追加とあります。

  it "should reject names that are too long" do
    long_name = "a" * 51
    long_name_user = User.new(@attr.merge(:name => long_name))
    long_name_user.should_not be_valid
  end

まだ、user.name の length に関する制限はないな。なので試験は微妙。

$ bundle exec rspec spec/
..................F

Failures:

  1) User should reject names that are too long
     Failure/Error: long_name_user.should_not be_valid
       expected valid? to return false, got true
     # ./spec/models/user_spec.rb:37:in `block (2 levels) in <top (required)>'

Finished in 0.66404 seconds
19 examples, 1 failure

Failed examples:

rspec ./spec/models/user_spec.rb:34 # User should reject names that are too long
$

app/models/user.rb を以下に修正。

class User < ActiveRecord::Base
  attr_accessible :name, :email

  validates :name, :presence => true,
                    :length   => { :maximum => 50 }
  validates :email, :presence => true
end

もう書いてあるママ、というカンジです。非常に分かりやすい。試験も当然パス。

$ bundle exec rspec spec/
...................

Finished in 0.92053 seconds
19 examples, 0 failures
$

6.2.3 Format validation

次は email な属性の format なのかな。以下な試験を追加せよ、とのこと。

  it "should accept valid email addresses" do
    addresses = %w[user@foo.com THE_USER@foo.bar.org first.last@foo.jp]
    addresses.each do |address|
      valid_email_user = User.new(@attr.merge(:email => address))
      valid_email_user.should be_valid
    end
  end

  it "should reject invalid email addresses" do
    addresses = %w[user@foo,com user_at_foo.org example.user@foo.]
    addresses.each do |address|
      invalid_email_user = User.new(@attr.merge(:email => address))
      invalid_email_user.should_not be_valid
    end
  end

なるほど。rspec 良いなぁ。いちいち日本語で書かなくて良いのが楽。とりあえず盛り込んで実行します。

$ bundle exec rspec spec/
....................F

Failures:

  1) User should reject invalid email addresses
     Failure/Error: invalid_email_user.should_not be_valid
       expected valid? to return false, got true
     # ./spec/models/user_spec.rb:52:in `block (3 levels) in <top (required)>'
     # ./spec/models/user_spec.rb:50:in `each'
     # ./spec/models/user_spec.rb:50:in `block (2 levels) in <top (required)>'

Finished in 0.79536 seconds
21 examples, 1 failure

Failed examples:

rspec ./spec/models/user_spec.rb:48 # User should reject invalid email addresses
$

失敗して当然、ということで以下にするらしい。

class User < ActiveRecord::Base
  attr_accessible :name, :email

  email_regex = /\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i

  validates :name, :presence => true,
                    :length   => { :maximum => 50 }
  validates :email, :presence => true,
                    :format   => { :with => email_regex }
end

微妙に正規表現のアレがナニだったりしてorz
試験も一応パス。

$ bundle exec rspec spec/
.....................

Finished in 0.73002 seconds
21 examples, 0 failures
$

6.2.4 Uniqueness validation

以下な試験を追加とのこと。

  it "should reject duplicate email addresses" do
    # Put a user with given email address into the database.
    User.create!(@attr)
    user_with_duplicate_email = User.new(@attr)
    user_with_duplicate_email.should_not be_valid
  end

試験は失敗するはず。

$ bundle exec rspec spec/
.....................F

Failures:

  1) User should reject duplicate email addresses
     Failure/Error: user_with_duplicate_email.should_not be_valid
       expected valid? to return false, got true
     # ./spec/models/user_spec.rb:60:in `block (2 levels) in <top (required)>'

Finished in 0.89521 seconds
22 examples, 1 failure

Failed examples:

rspec ./spec/models/user_spec.rb:56 # User should reject duplicate email addresses
$

app/models/user.rb に uniq なナニを追加。

class User < ActiveRecord::Base
  attr_accessible :name, :email

  email_regex = /\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i

  validates :name, :presence => true,
                    :length   => { :maximum => 50 }
  validates :email, :presence => true,
                    :format   => { :with => email_regex },
                    :uniqueness => true
end

これだけでは足らんらしい。大文字小文字なナニもなんとかなる模様。試験が以下。

  it "should reject email addresses identical up to case" do
    upcased_email = @attr[:email].upcase
    User.create!(@attr.merge(:email => upcased_email))
    user_with_duplicate_email = User.new(@attr)
    user_with_duplicate_email.should_not be_valid
  end

試験にはパスしません。

$ bundle exec rspec spec/
.....................FF

Failures:

  1) User should reject duplicate email addresses
     Failure/Error: user_with_duplicate_email.should_not be_valid
       expected valid? to return false, got true
     # ./spec/models/user_spec.rb:60:in `block (2 levels) in <top (required)>'

  2) User should reject email addresses identical up to case
     Failure/Error: user_with_duplicate_email.should_not be_valid
       expected valid? to return false, got true
     # ./spec/models/user_spec.rb:67:in `block (2 levels) in <top (required)>'

Finished in 1.11 seconds
23 examples, 2 failures

Failed examples:

rspec ./spec/models/user_spec.rb:56 # User should reject duplicate email addresses
rspec ./spec/models/user_spec.rb:63 # User should reject email addresses identical up to case
$

で、以下を app/models/user.rb に追加。というか修正。

class User < ActiveRecord::Base
  attr_accessible :name, :email

  email_regex = /\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i

  validates :name, :presence => true,
                    :length   => { :maximum => 50 }
  validates :email, :presence => true,
                    :format   => { :with => email_regex },
                    :uniqueness => { :case_sensitive => false }
end

試験パスするんですが case_sensitive というナニは引数になるのかな。

$ bundle exec rspec spec/
.......................

Finished in 4.21 seconds
23 examples, 0 failures
$

_The uniqueness caveat_という節。もの凄くレアなナニに対応する模様。

D’oh! But what can go wrong? Here’s what:

1. Alice signs up for the sample app, with address alice@wonderland.com.
2.Alice accidentally clicks on “Submit” twice, sending two requests in quick succession.
3.The following sequence occurs: request 1 creates a user in memory that passes validation, request 2 does the same, request 1’s user gets saved, request 2’s user gets saved.
4.Result: two user records with the exact same email address, despite the uniqueness validation.

このあたりは、_へー_という印象だな。

$ rails generate migration add_email_uniqueness_index
      invoke  active_record
      create    db/migrate/20110919112931_add_email_uniqueness_index.rb
$

あ、これを以下にするんか。

class AddEmailUniquenessIndex < ActiveRecord::Migration
  def self.up
    add_index :users, :email, :unique => true
  end

  def self.down
    remove_index :users, :email
  end
end

users および email 属性は unique であることを保証されている、という理解で良いかな。で、これおを元に migrate しておく、と。

$ bundle exec rake db:migrate
==  AddEmailUniquenessIndex: migrating ========================================
-- add_index(:users, :email, {:unique=>true})
   -> 0.0010s
==  AddEmailUniquenessIndex: migrated (0.0011s) ===============================

$

どうもこれで double click 問題はなんとかなってるらしいけど、もうちょっと続きがあるみたいです。

今日はもう限界ぽ

6.3 Viewing users は明日ってことで。