ペアプロ1日目: Jack さんと rails 3 と rspec と

はじめに

10月30日土曜日、その日は朝から雨風が強かった。日本付近を台風が通りすぎており、週末は大変そうだという話題が、数日前から twitter のタイムライン上に流れていた。

そんな中、ぼくらは秋葉原の喫茶店に集合し、4人掛けの席のとなりに座って何やら話を始めた。喫茶店で、男どうしが同じ側に座って、一台のパソコンの画面を眺めている。ちょっと不思議な光景かもしれない。けれども、これがぼくらのペアプロのスタイルだ。

今回のお相手

今回お相手してくださったのは、Jxck さん。86世代よりちょっと年上の方だ。Jxck さんとの出会いは、今年の XP 祭り。ランチをご一緒させていただいたのと、懇親会で立ちながら語りあったのを覚えている。

XP 祭りでの懇親会で、すごく印象に残ったのが、彼の情熱だ。まず、ペアプロの楽しさを伝えたいという事がビンビンと伝わってきた。さらに、その楽しさというのは、その道の権威が前で偉そうに話す事で伝わるのではなくて、現場の人たちが草の根的に伝わっていくものだ、ということを熱く語ってくれた。その後の twitter での Jxck さんの一言も素晴らしい。


ペアプロ日記をつけるなら、初回のお相手は彼にお願いしなければならない。そう思わずにいられないアツい人だ。もう既に、イベントで知り合った方々と、個人的に何度かペアプロをしているらしい。頼もしい限りだ。

今回のお題

Rails 3 で簡単なブログを作成することにした。まずはチュートリアルにあるようなアプリをつくること、ただ、それだけだと TDD をするのにもの足りないので、何かロジックを入れようということを、事前に Skype で話し合って決めた。

具体的には、まずは、編集ー確認ー表示といった簡単な構成で簡単なブログを作成する。そして、ポストを解析して頻出単語を表示するといったロジックを追加してみる、といった感じにまとまった。

出来上がったスペックは、以下のような感じだ。

ここでは、事前に話し合ったロジックの追加の他に、バリデーションのスペックも追加してある。

使った道具

事前に、rvm で、Jxck さんの Mac Book に、ruby 1.9.2、rails 3 を入れておいてもらった。

その上に Rails 3 のバンドルとして、

を追加した。

また、ソースコードのバージョン管理には、 git を使った。

Jxck さんの Mac Book 上の emacs で作業した。あとは、Jxck さんが用意してくださった、ホワイトボードが大変に役に立った。


実際の作業

アプリの作成、前準備

まずは、アプリを作成する。今回は rspec を使用するので、 "-T" を指定して、テストを作成しないようにした。

$ rails new miniblog -T

ここで、 Gemfile を書き換えた。結局、今回は autotest は使っていないが、一応追加してある。

miniblog/Gemfile

source 'http://rubygems.org'

gem 'rails', '3.0.1'

gem 'sqlite3-ruby', :require => 'sqlite3'

group :development, :test do
  gem 'rspec','>=2.0.0'
  gem 'rspec-rails','>=2.0.0'
  gem 'autotest'
  gem 'webrat'
end

バンドルのインストール。

$ bundle install

その後、 rspec のヘルパを追加する。

$ rails g rspec:install

続いて、以下のように config に設定を追記した。

miniblog/config/application.rb

require File.expand_path('../boot', __FILE__)

require 'rails/all'

# If you have a Gemfile, require the gems listed there, including any gems
# you've limited to :test, :development, or :production.
Bundler.require(:default, Rails.env) if defined?(Bundler)

module Miniblog
  class Application < Rails::Application

  # (省略)

    # Configure rspec
    config.generators do |g|
      g.test_framework  :rspec, :fixture => true
    end

  end
end

この辺りは、 ukstudio さんMasatomo Nakano さんのブログを参考にしたように記憶している。

これで、準備完了だ。

Scaffold

scaffold 前にコミットを作成しておいた。そうすることで、やり直しがきく。実際に、今回も scaffold を一回やり直すことになり、バージョン管理の恩恵にあづかることができた。

$ rails g scaffold post title:string content:text 

scaffold を作った後に、dbをマイグレーションする。

$ rake db:migrate

この時点で、既にブログの投稿はできるようになっている。

はじめの一歩

いよいよ、このペアでの初めてのコーディング。

はじめの一歩として、まずは、ペアの歩調を合わせるために、何かの資料をもとにテストを書いてみようということになった。そこで、登場したのが、かくたにさん、もろはしさんの 「スはスペックのス」るびまに載ったこの記事は、日本語で rails + rspec の事について調べるのであれば一度は行き着くものだろう。この記事には、モデルのテストの例としてバリデーションのテストが書かれている。まずは、この記事と同じ事をやるのを、はじめの一歩としよう、ということになった。

まずは、マイグレーションファイルを、タイトル、本文ともに null を許さないように書き換える。

miniblog/db/migrate/20101030044650_create_posts.rb

class CreatePosts < ActiveRecord::Migration
  def self.up
    create_table :posts do |t|
      t.string :title, :null => false
      t.text :content, :null => false

      t.timestamps
    end
  end

  def self.down
    drop_table :posts
  end
end

そして、データベースをマイグレートする。

$ rake db:migrate

その上で、バリデーションのスペックを定義した。

miniblog/db/spec/models/post_spec.rb

describe Post, "モデル" do
  describe "バリデーション" do
    fixtures :posts

    before(:each) do
    end

    describe "タイトルも本文も入っている時" do
      it "成功する" do
        post = posts(:full)
        post.should be_valid
      end
    end

    describe "タイトルも本文も入っていない時" do
      it "失敗する" do
        post = posts(:empty)
        post.should_not be_valid
      end
    end

    describe "タイトルだけ入っている時" do
      it "失敗する" do
        post = posts(:title_only)
        post.should_not be_valid
      end
    end

    describe "本文だけ入っている時" do
      it "失敗する" do
        post = posts(:content_only)
        post.should_not be_valid
      end
    end
  end
end

ここでは、以下のフィクスチャを使っている。

miniblog/spec/fixtures/posts.yml

full:
  title: "test"
  content: "This is a test post."

empty:
  title:
  content:

title_only:
  title: "test"
  content:

content_only:
  title:
  content: "This is a test post."

このスペックを実行すると Red になるので、これを Green にするためにバリデーションを有効にする。そのために、モデルを書き換えた。

miniblog/app/models/post.rb

class Post < ActiveRecord::Base
  validates_presence_of :title
  validates_presence_of :content
end

ここまでは、記事と同じ道を歩んできたことになる。

サーバを実行し、空のデータを入れてみると実際の画面はこのようになった。ちゃんと、バリデーションが効いているようだ。

$ rails s


続いて Model のロジック

続いて、本日のメインディッシュである、ロジックを記述していく。今回は、 TDD でペアプロをするということが第一の主眼なので、以下のように簡単な仕様とした。

  • ブログ本文はアルファベットで書かれ、単語は空白によって区切られる
  • 句読点を削除したりなどといった処理はしない
  • 大文字と小文字が違うと違う単語とする

また、今回は、表示の度に頻出単語の解析を毎回おこなうことにした。

部分のテストだ。まずは、 Blog.count_wards() のスペックを定義する。そのまま、仮実装から三角測量に進もうとするも、もしかしたら、一歩目が大きいかもということで、いったん、 pending し、とりあえず、 Blog.split_content() を作成することにした。

結局、モデルのコードは以下のような感じになった。

miniblog/spec/models/post_spec.rb

# -*- coding: utf-8 -*-
require 'spec_helper'

describe Post, "モデル" do
  describe "バリデーション" do
    fixtures :posts

    before(:each) do
    end

    describe "タイトルも本文も入っている時" do
      it "成功する" do
        post = posts(:full)
        post.should be_valid
      end
    end

    describe "タイトルも本文も入っていない時" do
      it "失敗する" do
        post = posts(:empty)
        post.should_not be_valid
      end
    end

    describe "タイトルだけ入っている時" do
      it "失敗する" do
        post = posts(:title_only)
        post.should_not be_valid
      end
    end

    describe "本文だけ入っている時" do
      it "失敗する" do
        post = posts(:content_only)
        post.should_not be_valid
      end
    end
  end

  describe Post, ".count_words()" do
    fixtures :posts

    before(:each) do
    end

    describe "本文に hoge が20回含まれる時" do
      it "1単語ぶんの配列を返す" do
        post = posts(:hoge_20)
        actual = post.count_words()
        expected = Array(
          "hoge" => 20
        )
        actual.should == expected
      end
    end

    describe "本文に hoge が15回, fuga が5回含まれる時" do
      it "2単語ぶんの配列を返す" do
        post = posts(:hoge_fuga)
        actual = post.count_words()
        expected = Array(
          "hoge" => 15,
          "fuga" => 5
        )
        actual.should == expected
      end
    end

    describe "本文に rails が4回, ruby が3回, on が2回, nutshell が1回含まれる時" do
      it "先頭3単語ぶんの配列を返す" do
        post = posts(:short_content)
        actual = post.count_words()
        expected = Array(
          "rails" => 4,
          "ruby" => 3,
          "on" => 2
        )
        actual.should == expected
      end
    end
  end

  describe Post, ".split_content()" do
    fixtures :posts
  
    before(:each) do
    end
  
    it "本文に含まれる単語をソートして返す" do
      post = posts(:short_content)
      actual = post.split_content()
      expected = ["nutshell",
                  "on", "on",
                  "rails", "rails", "rails", "rails",
                  "ruby", "ruby", "ruby"]
      actual.should == expected
    end
  end
end

miniblog/app/models/post.rb

# -*- coding: utf-8 -*-
class Post < ActiveRecord::Base
  validates_presence_of :title
  validates_presence_of :content

  def count_words
    words = split_content
    word_counts = {}

    words.map do |w|
      if word_counts[w]
        word_counts[w] += 1
      else
        word_counts[w] = 1
      end
    end

    results = word_counts.sort do |e1, e2|
      e1[1] <=> e2[1]
    end

    results.reverse[0,3]
  end

  def split_content
    content.split.sort
  end
end
最後に View のテスト

View のスペックは、こんな感じだ。

miniblog/spec/views/posts/show.html.erb_spec.rb

# -*- coding: utf-8 -*-
require 'spec_helper'

describe "posts/show.html.erb" do
  before(:each) do
    @post = assign(:post, stub_model(Post,
      :title => "short_content",
      :content => "on rails ruby ruby rails rails ruby rails on nutshell"
    ))
  end

  it "頻出単語を表示する" do
    render

    rendered.should have_selector('ul')
    rendered.should have_selector('li')
    rendered.should have_selector('span')

    rendered.should contain("ruby")
    rendered.should contain("rails")
    rendered.should contain("on")
    rendered.should contain("(4)")
    rendered.should contain("(3)")
    rendered.should contain("(2)")
  end
end
とりあえず完成!

ここまでで、事前に打ち合わせした機能はひととり完成した。 rails 公式サイトのトップページの英文テキストを投稿してみたところ、以下のように表示された。やはり、 "rails" が再頻出単語のようだ。

なお、今回のコードは Jxck さんが github 上に push してくださった。自分はそれを fork している。

http://github.com/tosikawa/mini-blog

まとめ

事前に、 Jxck さんと確認した、「今回の一番の目的はTDDでペアプロ」という部分は、なんとか達成できたと思う。そして、Rails 3 のテストのしやすさにも、ふれることができた。

Jxck さんご提案の KPTChangeLog 形式で書くのも良かった。その KPT の中に、「"やること"は決めておくとすんなり始められて達成感もある」ということを Jxck さんが書いてくださった。決めておいたことが出来たことと、達成感を共有できたことが何よりも良かった。

やっぱり、ペアプロって楽しいと思う。「ペアプロの楽しさコツを伝えていきたい」という Jxck さんのつぶやきが胸に響く。

あとは、直接、内容とは関係ないけれども、gitについて色々議論した。 git スゲェな、ということで合意できたと思う。

あっという間だった。いつの間にか台風は過ぎ去り、熱帯低気圧となっていたようだ。

*1:自分の Ubuntu 上で動作させようと思ったときに、webrat のコンパイルで少しつまずいた。 libxml2-dev と libxslt1-dev が必要なようだ。