ペアプロ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 は使っていないが、一応追加してある。
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 に設定を追記した。
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 を許さないように書き換える。
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
その上で、バリデーションのスペックを定義した。
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
ここでは、以下のフィクスチャを使っている。
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 にするためにバリデーションを有効にする。そのために、モデルを書き換えた。
class Post < ActiveRecord::Base validates_presence_of :title validates_presence_of :content end
ここまでは、記事と同じ道を歩んできたことになる。
サーバを実行し、空のデータを入れてみると実際の画面はこのようになった。ちゃんと、バリデーションが効いているようだ。
$ rails s
続いて Model のロジック
続いて、本日のメインディッシュである、ロジックを記述していく。今回は、 TDD でペアプロをするということが第一の主眼なので、以下のように簡単な仕様とした。
- ブログ本文はアルファベットで書かれ、単語は空白によって区切られる
- 句読点を削除したりなどといった処理はしない
- 大文字と小文字が違うと違う単語とする
また、今回は、表示の度に頻出単語の解析を毎回おこなうことにした。
部分のテストだ。まずは、 Blog.count_wards() のスペックを定義する。そのまま、仮実装から三角測量に進もうとするも、もしかしたら、一歩目が大きいかもということで、いったん、 pending し、とりあえず、 Blog.split_content() を作成することにした。
結局、モデルのコードは以下のような感じになった。
# -*- 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
# -*- 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 のスペックは、こんな感じだ。
# -*- 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 している。
まとめ
事前に、 Jxck さんと確認した、「今回の一番の目的はTDDでペアプロ」という部分は、なんとか達成できたと思う。そして、Rails 3 のテストのしやすさにも、ふれることができた。
Jxck さんご提案の KPT を ChangeLog 形式で書くのも良かった。その KPT の中に、「"やること"は決めておくとすんなり始められて達成感もある」ということを Jxck さんが書いてくださった。決めておいたことが出来たことと、達成感を共有できたことが何よりも良かった。
やっぱり、ペアプロって楽しいと思う。「ペアプロの楽しさコツを伝えていきたい」という Jxck さんのつぶやきが胸に響く。
あとは、直接、内容とは関係ないけれども、gitについて色々議論した。 git スゲェな、ということで合意できたと思う。
あっという間だった。いつの間にか台風は過ぎ去り、熱帯低気圧となっていたようだ。