Everyday Rails - RSpecによるRailsテスト入門

Everyday Rails - RSpecによるRailsテスト入門
Everyday Rails - RSpecによるRailsテスト入門
Buy on Leanpub

日本語版のまえがき

私は好運です。

英語は私の母国語です。そして英語は技術文書の非公式な共通言語になっているようです。私は数多くの本やブログ、スクリーンキャストで勉強し、テスト駆動開発とRSpecを理解することができました。そしてついに、自分でその本を書くこともできました。私には想像することしかできませんが、世界中のソフトウェア開発者の多くは新しいプログラミング言語を学ぶだけでなく、関連する情報源が書かれている外国語もがんばって学ぶ必要があるんですよね。

そして、私はまたもや好運であり、大変嬉しく思っています。なぜかといえば、同じRubyistである伊藤淳一さん、魚振江さん、秋元利春さんが日本語で読みたがっているプログラマのために、 Everyday Rails Testing with RSpec をがんばって翻訳してくれたからです。彼らは本書を新しい読者に届けてくれただけでなく、今後のバージョンの改善に役立つ貴重なフィードバックも返してくれました。

読者のみなさんが淳一さん、振江さん、利春さんの努力の成果を私と同じぐらい楽しんで、感謝することを願っています。そして、あなたの今後のRails開発にも好運が訪れることを願っています!

 

Aaron Sumner

Author

Everyday Rails Testing with RSpec

この版のまえがき

RSpec 3版の Everyday Rails - RSpecによるRailsテスト入門 がついに完成しました!RSpec 3版では多くの変更点を変更しました。みなさんには待った甲斐があったと思ってもらえると嬉しいです。

以前のアップデートと同様に、サンプルアプリケーションは再作成しています。再作成の際にはRailsとRSpec、それに本書で使用したその他のgemの最新バージョンを使用しました。さらに、いくつかのセクションを大きく加筆しています。一番大きく加筆したのは第10章の その他のテスト です。また、必要に応じてRSpec 3とRails 4.1の新機能を活かした加筆修正も行っています。公開前に自分で何度も読み直し、問題が無いことを確認したつもりですが、もしかすると読者のみなさんは内容の誤りに気付いたり、別のもっと良い方法を知っていたりするかもしれません。間違いを見つけたり、何か良いアイデアがあったりする場合は、このリリース用の GitHub issues にぜひ報告してください。すぐに対処します。(訳注: 日本語版のフィードバックはこちらからお願いします。https://leanpub.com/everydayrailsrspec-jp/feedback

改めてみなさんに感謝します。みなさんにこの版を気に入ってもらえることを願っています。そしてGithubやTwitter、Eメールでみなさんの感想が聞けることも楽しみにしています。

1. イントロダクション

Ruby on Railsと自動テストは相性の良い組み合わせです。Railsにはデフォルトのテストフレームワークが付いてきますが、もしそれが好みでなければ自分が好きなものに置き換えることができます。私がこれを書いている時点でRuby Toolboxだけでも Unit Test Frameworks カテゴリに17のプロジェクトが挙がってきます。つまり、テストはRailsで大変重要視されています。とはいえ、Railsでテストを全く書かずに開発する人や、書いたとしてもモデルのバリデーション用にたった数個のスペックしか書かないような人もたくさんいます。

これにはいくつかの理由があると私は考えています。人によってはRubyやwebフレームワークは新しい技術であり、そこへさらに新しい技術が増えるのは彼らにとって余計な仕事になるのかもしれません。もしくは時間の制約が問題になっている可能性もあります。テストを書く時間が増えることによって、顧客や上司から要求されている機能に費やす時間が減ってしまうからです。もしくはブラウザのリンクをクリックするのが “テスト” であるという習慣から抜け出せなくなっているだけかもしれません。

私も同じでした。私は自分のことを正真正銘のエンジニアだとは思っていませんが、解決すべき問題を持っているという点ではエンジニアと同じです。そしてたいていの場合、ソフトウェアを構築することがその解決策になっています。私は1995年からwebアプリケーションを開発しています。ただし、予算の乏しい公共セクターのプロジェクトを一人で担当することがほとんどです。小さいころにBASICをさわったり、大学でC++をちょっとやったり、社会人になってから入った2社目の会社で役に立たないJavaのトレーニングを一週間ほど受講したりしたことはありましたが、ソフトウェア開発のまともな教育というものは全く受けたことがありません。実際、私は2005年までPHPで書かれたひどいスパゲティプログラムをハックしていて、それからようやくwebアプリケーションのもっと上手な開発方法を探し始めました。

私はかつてRubyを触ったことはありましたが、真剣に使い始めたのはRailsが注目を集め出してからです。RubyやRailsには学習しなければいけないことがたくさんありました。たとえば、新しい言語や アーキテクチャ 、よりオブジェクト指向らしいアプローチ等々です(Railsにおけるオブジェクト指向を疑問に思う人がいるかもしれませんが、フレームワークを使っていなかったころに比べれば、私のコードはずっとオブジェクト指向らしくなりました)。このように新しいチャレンジはいくらか必要だったものの、それでもフレームワークを使わずに開発していた時代に比べると、ずっと短い時間で複雑なアプリケーションが作れました。こうして私は夢中になったのです。

とはいえ、Railsに関する初期の書籍やチュートリアルは、テストのような良いプラクティスよりもスピード(15分でブログアプリケーションを作る!)にフォーカスしていました。テストの説明は全くなかったか、説明されていたとしても、たいてい最後の方に一章だけしか用意されてませんでした。最近の書籍やweb上の情報源ではそういう傾向が間違いであったことを認めていますし、アプリケーション全体をテストする方法を初めから説明しているように思います。加えて、テストについて 専門的に 書かれた本はたくさんありますが、テストの実践方法をしっかり身につけていないと開発者(特にかつての私と同じような道を歩んでいる開発者)の多くは一貫したテスト戦略を考えられないかもしれません。

本書のゴールは 私の 役に立っている一貫した戦略をあなたに伝えることです。そしてその戦略を使って、 あなたも 一貫したテスト戦略をとれるように願っています。

なぜRSpecなのか?

私は基本的に他のテストフレームワークを悪く言うつもりはありません。実際、単体のRubyライブラリを書く時はMiniTestをよく使っています。しかし、Railsアプリケーションをテストするときは、どういうわけかRSpecを使い続けています。

私にはコピーライティングとソフトウェア開発のバックグラウンドがあるせいかもしれませんが、RSpecを使うと読みやすいスペックが簡単に書けます。これが私にとって一番の決め手でした。のちほど本書でお話ししますが、技術者ではなくても大半の人々がRSpecで書かれたスペックを読み、その内容を理解できたのです。

対象となる読者

もしRailsがあなたにとって初めてのwebアプリケーションだったり、これまでのプログラマ人生でテストの経験がそれほどなかったりするなら、本書はきっと良い入門書になると思います。もし、あなたが 全くの Rails初心者なら、この Everyday Rails - RSpecによるRailsテスト入門 を読む前に、Michael Hartlの Railsチュートリアルオンラインで読めます)や、Daniel Kehoeの Learn Ruby on Rails 、もしくはSam Rubyの RailsによるアジャイルWebアプリケーション開発 といった書籍でRailsの開発や基礎的なテスト方法を学習しておいた方が良いかもしれません。なぜなら、本書はあなたがすでにRailsの基礎知識を身につけていることを前提としているからです。言い換えると、本書ではRailsの使い方は説明しません。また、Railsに組みこまれているテストツールを最初から紹介することもしません。ただし、RSpecといくつかの追加ライブラリはインストールします。追加ライブラリはテストのプロセスをできるだけ理解しやすく、そして管理しやすくするために使います。

もしあなたのRails開発経験があまり長くなく、本番運用しているアプリケーションが一つか二つぐらいで、なおかつテストにはまだ馴染めていないのなら、まさに本書は最適です!私もかつてはずっとあなたと同じでしたが、私は本書で紹介するようなテクニックでテストカバレッジを向上させ、テスト駆動開発者らしい考え方を身につけることができました。あなたも私と同じようにこうしたテクニックを身につけてくれることを私は願っています。

ところで、本書で前提としている読者の知識や経験を具体的に挙げると、次のようになります。

  • Railsで使われているModel-View-Controllerアーキテクチャ
  • gemの依存関係を管理するBundler
  • Rakeタスクの実行方法
  • 基本的なコマンドラインの使い方
  • リポジトリのブランチを切り替えられるぐらいのGit知識

一方、上級者に関していうと、もしあなたがTest::UnitやMiniTest、もしくはRSpecそのものに慣れていて、十分なカバレッジが出せるような満足のいくワークフローを確立している場合、本書を読むことでテストのアプローチを多少改善できるかもしれません。ですが、正直に言って、現時点であなたは自動テストの重要性を十分理解できているはずですし、本書から得られる知識はほとんど必要ないものかもしれません。本書はテスト理論に関する本ではありません。また、パフォーマンス問題を深く掘り下げるわけでもありません。本書を読むよりも、他の書籍を読んだ方が長い目で見たときに役立つかもしれません。

私が考えるテストの原則

Railsをテストする 正しい 方法というテーマで議論をすると、プログラマの間で大げんかが始まるかもしれません。まあ、VimとEmacsの論争ほど激しくないとは思いますが、それでもRubyistの中で不穏な空気が流れそうです。実際、David Heinmeier-HansenはRailsconf 2014でTDDは「死んだ」と発言し、Railsのテストについて新たな議論を巻き起こしました。

確かにテストの正しい方法は存在します。しかし私に言わせれば、テストに関してはその 正しさ の度合いが異なるだけです。

Rubyのテスト駆動/ビヘイビア駆動開発コミュニティからはさらに異論が巻き起こるかもしれませんが、私のアプローチでは次のような原則に焦点を当てています。

  • テストは信頼できるものであること
  • テストは簡単に書けること
  • テストは簡単に理解できること

この三つの要素を意識しながら実践すれば、アプリケーションにしっかりしたテストスイートができあがっていきます。さらに、昨今のテスト駆動開発の意味がなんであるにせよ、あなたが本物のテスト駆動開発者に近づいていくことは言うまでもありません。

もちろん、それと引き替えに失うものもあります。具体的には次のようなことです。

  • スピードは重視しません(ただし、これについてはのちほど説明します)。
  • テストの中では過度にDRYなコードを目指しません。なぜならテストにおいてはDRYでないコードは必ずしも悪とは限らないからです。この点ものちほど説明します。

とはいえ結局、一番大事なことはテストを書くことです。最適化されているとは言いがたいテストであっても、信頼性が高く、理解しやすいテストを書くことが大事な出発点になります。かつて私はたくさんアプリケーション側のコードを書き、ブラウザをあちこちクリックすることで “テスト” し、うまく動くことを祈っていました。しかし、テストを書くようになって、こうした問題をついに乗り越えることができました。完全に自動化されたテストスイートを利用すれば、開発を加速させ、潜在的なバグや境界値に潜む問題をあぶり出すことができるのです。

そして、このアプローチこそがこれから本書で説明していく内容です。

本書の構成

本書 Everyday Rails - RSpecによるRailsテスト入門 では、標準的なRails 4.1アプリケーションが全くテストされていない状態から、RSpec 3.1を使ってきちんとテストされるまでを順に説明していきます。本書は次のようなテーマに分けられています。

  • あなたが今読んでいるのが第1章 イントロダクション です。
  • 第2章 RSpecのセットアップ では、いくつかの便利なテスト用ツールを使って、新規、もしくは既存のRailsアプリケーションでRSpecが使えるようにセットアップします。
  • 第3章 モデルスペック では信頼性の高い単体テストを通じてモデルをテストしていきます。
  • 第4章 ファクトリを使ったテストデータの生成 ではテストデータの生成を簡単にしてくれるファクトリについて説明します。
  • 第5章 コントローラスペックの基礎 ではコントローラのテストについて最初の説明を行います。
  • 第6章 高度なコントローラスペック では認証と認可のレイヤが正しく機能していること、すなわちアプリケーションのデータがちゃんと守られていることをコントローラスペックで確認します。
  • 第7章 コントローラスペックのクリーンアップ ではスペックのリファクタリングについて最初の説明を行います。このリファクタリングでは可読性を保ったまま冗長なコードを減らしていきます。
  • 第8章 フィーチャスペック ではフィーチャスペックを使った統合テストを見ていきます。フィーチャスペックを使うと、アプリケーションの様々なパーツが他のパーツと協調して動作することをテストできます。
  • 第9章 スペックの高速化 ではリファクタリングのテクニックとパフォーマンスを念頭に置いたテストの実行方法について説明します。
  • 第10章 その他のテスト ではメール送信やファイルアップロード、時間に依存する機能、APIといった、これまでに説明してこなかった機能のテストについて説明します。
  • 第11章 テスト駆動開発に向けて ではステップ・バイ・ステップ形式でテスト駆動開発の実践方法をデモンストレーションします。
  • そして、第12章 最後のアドバイス で、これまで説明してきた内容を全部まとめます。

各章にはステップ・バイ・ステップ形式の説明を取り入れています。これは私が自分自身のソフトウェアでテストスキルを上達させたのと同じ手順になっています。また、多くの章はQ&Aセクションで締めくくり、そのあとに演習問題が続きます。この演習問題はその章で習ったテクニックを自分で使ってみるために用意しています。繰り返しますが、あなた自身のアプリケーションでこうした演習問題に取り組んでみることを私は強く推奨します。一つはチュートリアルの内容を復習するためで、もう一つはあなたが学んだことをあなた自身の状況で応用するためです。本書では一緒にアプリケーションを作っていくのではなく、単にコードのパターンやテクニックを掘り下げていくだけです。ここで学んだテクニックを使い、あなた自身のプロジェクトを改善させましょう!

サンプルコードのダウンロード

本書のサンプルコードはGitHubにあります。このアプリケーションではテストコードも完全に書かれています。

もしGitの扱いに慣れているなら(Rails開発者ならきっと大丈夫なはず)、サンプルコードをあなたのコンピュータにクローン(clone)することもできます。各章の成果物はそれぞれブランチを分けています。各章のソースを開くと完成後のコードが見られます。本書を読みながら実際に手を動かす場合は、一つ前の章のソースを開くと良いでしょう。各ブランチには章番号を振ってありますが、各章の最初でもチェックアウトすべきブランチを伝えていきます。

Gitの扱いに慣れていなくても各章のサンプルコードをダウンロードすることは可能です。まずGitHubのプロジェクトページを開いてください。それからブランチセレクタでその章のブランチを選択します。

最後にZIPダウンロードボタンをクリックします。クリックするとソースコードをコンピュータに保存できます。

コードの方針

このアプリケーションは次の環境で動作します。

  • Rails 4.1: 最新バージョンのRailsが本書のメインターゲットです。とはいえRails 3.0以上であれば本書で紹介しているテクニックは適用できるはずです。サンプルコードによっては違いが出るかもしれませんが、差異が出そうな箇所はできる限り伝えていきます。
  • Ruby 2.1: 1.9や2.0を使っていても大きな差異はないと思います。しかし、今でも1.8を使っているのであれば、本書を読み進めるのが難しくなると思うのでお薦めできません。
  • RSpec 3.1: RSpec 3.0は2014年の春にリリースされました。その数ヶ月後にRSpec 3.1が登場しましたが、大半は3.0と互換性が保たれています。RSpec 3.1はRSpec 2.14に近いシンタックスになっていますが、多少異なる点もあります。

本書で使用しているバージョン固有の用法等があれば、できる限り伝えていきます。もし上記のツールのどれかで古いバージョンを使っているなら、本書の以前の版をダウンロードしてください。以前の版はLeanpubからダウンロードできます。個々の機能はきれいに対応しませんが、バージョン違いに起因する基本的な差異は理解できるもしれません。

もう一度言いますが、本書はよくありがちなチュートリアルではありません! 本書に載せているコードはアプリケーションをゼロから順番に作ることを想定していません。そうではなく、テストのパターンと習慣を学習し、あなた自身のRailsアプリケーションに適用してもらうことを想定しています。言いかえると、コードをコピー&ペーストすることができるとはいえ、そんなことをしても全くあなたのためにならないということです。あなたはこのような学習方法をZed ShawのLearn Code the Hard Wayシリーズで知っているかもしれません。 Everyday Rails - RSpecによるRailsテスト入門 はそれと全く同じスタイルではありませんが、私はZedの考え方に同意しています。すなわち、何か学びたいものがあるときはWebサイトや電子書籍からコピー&ペーストするのではなく、自分でコードをタイピングした方が良い、ということです。

間違いを見つけた場合

完璧な人間はいません。私は特に完璧ではありません。私はたくさんの時間と労力をつぎ込んで Everyday Rails - RSpecによるRailsテスト入門 をできる限り間違いのない本にしようとしてきましたが、私が見落とした間違いにあなたは気付くかもしれません。そんなときはGitHubのissuesページで間違いを報告したり詳細を尋ねたりしてください。 https://github.com/everydayrails/rails-4-1-rspec-3-0/issues(訳注: 日本語版に関するフィードバックは https://leanpub.com/everydayrailsrspec-jp/feedback から投稿してください。 )

サンプルアプリケーションについて

本書で使用するサンプルアプリケーションは本当に質素で、本当にちっぽけな連絡先管理ツールです。このアプリケーションは企業Webサイトの一部なのかもしれません。提供する機能は名前とメールアドレスと電話番号を一覧表示することと、単純な頭文字検索です。これらの機能はサイトに訪問すれば誰でも使えます。ログインが必要になるのは新しい連絡先を追加したり、既存の連絡先を変更したりする場合です。最後に、ユーザーは管理者権限を持っていないと新しいユーザーをシステムに追加することができません。

さて、私はここまで意図的にRailsのデフォルトのジェネレータだけを使ってアプリケーション全体を作成しました( 01_untested ブランチにあるサンプルコードを参照)。つまりこれは test ディレクトリに何も変更していないテストファイルとフィクスチャがそのまま格納されているということです。この時点で rake test を実行すると、いくつかのテストはそのままでパスするかもしれません。しかし、本書はRSpecの本です。もっと良いソリューションをこれから作っていくのでtestフォルダは用無しになります。RSpecが使えるようにRailsをセットアップし、もっと立派なテストスイートを構築していきましょう。これが今から私たちが順を追って見ていく内容です。

まず最初にすべきことは、RSpecを使うようにアプリケーションの設定を変更することです。Railsのジェネレータでコードを追加するときは、毎回適切なスペック(それにいくつかの便利なファイル)を作成するようにアプリケーションを設定します。

では始めましょう!

3. モデルスペック

堅牢で信頼性の高いテストスイートを構築するために必要なツールはすべて揃いました。今度はそのツールを使う時です。まずアプリケーションのコアとなる部分、すなわちモデルから始めてみましょう。

本章では次のようなタスクを完了させます。

  • まず既存のモデルに対してモデルスペックを作ります。ここでは Contact モデルがその対象になります。
  • それからモデルのバリデーション、クラスメソッド、インスタンスメソッドのテストを書きます。テストを作りながらスペックの整理もします。

既存のモデルがあるので、最初のスペックファイルは手作業で追加します。アプリケーションに新しいモデルを追加する場合は(大丈夫、第11章でもやります)、第2章で設定した便利なRSpecジェネレータが私たちに代わって仮のファイルを作ってくれます。

モデルスペックの構造

私はモデルレベルのテストが一番学習しやすいと思います。なぜならモデルをテストすればアプリケーションのコアとなる部分をテストすることになるからです。このレベルのコードが十分にテストされていることが必要不可欠です。堅牢な土台作りはコードレベル全体の信頼性を高めるための第一歩です。

はじめに、モデルスペックには次のようなテストを含めましょう。

  • 有効な属性が渡された場合、モデルの create メソッドが正常に完了すること
  • バリデーションを失敗させるデータであれば、正常に完了しないこと
  • クラスメソッドとインスタンスメソッドが期待通りに動作すること

良い機会なので、ここでモデルスペックの基本構成を見てみましょう。スペックの記述をアウトラインと考えるのが便利です。たとえば、メインとなる Contact モデルの要件を見てみましょう。

describe Contact do
  # 姓と名とメールがあれば有効な状態であること
  it "is valid with a firstname, lastname and email"
  # 名がなければ無効な状態であること
  it "is invalid without a firstname"
  # 姓がなければ無効な状態であること
  it "is invalid without a lastname"
  # メールアドレスがなければ無効な状態であること
  it "is invalid without an email address"
  # 重複したメールアドレスなら無効な状態であること
  it "is invalid with a duplicate email address"
  # 連絡先のフルネームを文字列として返すこと
  it "returns a contact's full name as a string"
end

このアウトラインはすぐあとに展開していきますが、初心者はここからたくさんのことが学べます。これは本当にシンプルなモデルのシンプルなスペックです。しかし、次のような4つのベストプラクティスを示しています。

  • 期待する結果をまとめて記述(describe)している。 このケースでは Contact モデルがどんなモデルなのか、そしてどんな振る舞いをするのかということを説明しています。
  • example( it で始まる1行)一つにつき、結果を一つだけ期待している。 私が firstnamelastnameemail のバリデーションをそれぞれ分けてテストしている点に注意してください。こうすれば、exampleが失敗したときに問題が起きたバリデーションを 特定 できます。原因調査のためにRSpecの出力結果を調べる必要はありません。少なくともそこまで細かく調べずに済むはずです。
  • どのexampleも明示的である。 技術的なことを言うと、 it のあとに続く説明用の文字列は必須ではありません。しかし、省略してしまうとスペックが読みにくくなります。
  • 各exampleの説明は動詞で始まっている。shouldではない。 期待する結果を声に出して読んでみましょう。 Contact is invalid without a firstname (名がなければ連絡先は無効な状態である)、 Contact is invalid without a lastname (姓がなければ連絡先は無効な状態である)、 Contact returns a contact’s full name as a string (連絡先は文字列として連絡先のフルネームを返す)。可読性は重要です!

こうしたベストプラクティスを念頭に置きながら Contact モデルのスペックを書いてみましょう。

モデルスペックを作成する

まず、 spec ディレクトリを開きます。必要であれば models という名前のサブディレクトリを作成します。そのサブディレクトリの中に contact_spec.rb という名前のファイルを作り、次のようなコードを追加しましょう。

spec/models/contact_spec.rb
 1 require 'rails_helper'
 2 
 3 describe Contact do
 4   # 姓と名とメールがあれば有効な状態であること
 5   it "is valid with a firstname, lastname and email"
 6   # 名がなければ無効な状態であること
 7   it "is invalid without a firstname"
 8   # 姓がなければ無効な状態であること
 9   it "is invalid without a lastname"
10   # メールアドレスがなければ無効な状態であること
11   it "is invalid without an email address"
12   # 重複したメールアドレスなら無効な状態であること
13   it "is invalid with a duplicate email address"
14   # 連絡先のフルネームを文字列として返すこと
15   it "returns a contact's full name as a string"
16 end

最初の行にある require 'rails_helper' に気をつけてください。そしてこれをタイプするのに慣れてください。今後すべてのスペックにこの行が含まれることになります。これはRSpec 3で新しく登場した内容です。以前のバージョンでは spec_helper と書いていました。Rails固有の設定はこのファイルに移動しています。その結果、 spec_helper はかなり小さくなりました。この内容は第9章でもう少し詳しく説明します。

詳細はこのあと追加していきますが、この状態でコマンドラインからスペックを実行すると(コマンドラインから bin/rspec とタイプしてください)、出力結果は次のようになります。

Contact
  is valid with a firstname, lastname and email
    (PENDING: Not yet implemented)
  is invalid without a firstname
    (PENDING: Not yet implemented)
  is invalid without a lastname
    (PENDING: Not yet implemented)
  is invalid without an email address
    (PENDING: Not yet implemented)
  is invalid with a duplicate email address
    (PENDING: Not yet implemented)
  returns a contact's full name as a string
    (PENDING: Not yet implemented)

Pending:
  Contact is valid with a firstname, lastname
    and email
    # Not yet implemented
    # ./spec/models/contact_spec.rb:4
  Contact is invalid without a firstname
    # Not yet implemented
    # ./spec/models/contact_spec.rb:5
  Contact is invalid without a lastname
    # Not yet implemented
    # ./spec/models/contact_spec.rb:6
  Contact is invalid without an email address
    # Not yet implemented
    # ./spec/models/contact_spec.rb:7
  Contact is invalid with a duplicate email address
    # Not yet implemented
    # ./spec/models/contact_spec.rb:8
  Contact returns a contact's full name as a string
    # Not yet implemented
    # ./spec/models/contact_spec.rb:9

Finished in 0.00105 seconds (files took 2.42 seconds to load)
6 examples, 0 failures, 6 pending

すばらしい!6つの保留中のexampleができました。まず最初のexampleから書き始めて、全部のスペックをパスさせましょう。

RSpecの新しい構文

2012年6月、RSpecの開発チームはそれまで使われてきた should に代わる新しい構文をバージョン2.11に追加しました。そうです、これは本書の完成版を最初にリリースしたわずか数日後の出来事でした。こうした変更に追い付いていくのは大変です!

この新しいアプローチを使うと古い should 構文で発生していた、いくつかの技術的な問題が起こりにくくなります。これからは「~が期待した結果と一致すべきだ/すべきでない(something should or should_not match expected output)」と言う代わりに、「私は~が~になる/ならないことを期待する(I expect something to or not_to be something else)」と言いましょう。

例として、このエクスペクテーション(expectation)を見てみましょう。このexampleの場合、2 + 1はいつでも3に等しいはずですよね?古いRSpecの構文ではこのように書きます。

# 2と1を足すと3になること
it "adds 2 and 1 to make 3" do
  (2 + 1).should eq 3
end

新しい構文ではテストする値を expect() メソッドに渡し、それに続けてマッチャを呼び出します。

# 2と1を足すと3になること
it "adds 2 and 1 to make 3" do
  expect(2 + 1).to eq 3
end

GoogleやStack OverflowでRSpecに関する質問を検索すると、今でも古い should 構文を使った情報をよく見かけると思います。この構文はRSpec 3でも動作しますが、使うと非推奨であるとの警告が出力されます。設定を変更すればこの警告を出力しないようにすることも できます が、そんなことはせずに新しい expect() 構文を学習した方が良いと思います。

では、実際のexampleではどうなるでしょうか? Contact モデルの最初のエクスペクテーションで使ってみましょう。

spec/models/contact_spec.rb
 1 require 'rails_helper'
 2 
 3 describe Contact do
 4   # 姓と名とメールがあれば有効な状態であること
 5   it "is valid with a firstname, lastname and email" do
 6     contact = Contact.new(
 7       firstname: 'Aaron',
 8       lastname: 'Sumner',
 9       email: 'tester@example.com')
10     expect(contact).to be_valid
11   end
12 
13   # 残りのexampleが続きます
14 end

この単純なexampleはRSpecの be_valid マッチャを使って、モデルが有効な状態を理解できているかどうかを検証しています。まずオブジェクトを作成し(このケースでは新しく作られているが保存はされていない Contact クラスのインスタンスを作成し、 contact という名前の変数に格納しています)、それからオブジェクトを expect に渡して、マッチャと比較しています。

さて、コマンドラインからもう一度RSpecを実行してみると( bin/rspec もしくは bundle exec rspec を使います。どちらを使うかは前の章で rspec の binstub をインストールしたかどうかによります。)・・・・exampleが一つパスしています!うまくいっているようです。ではもっとたくさんコードをテストしていきましょう。

バリデーションをテストする

自動テストに切り替えていくならバリデーションから始めると良いでしょう。バリデーションのテストはたいてい1~2行で書けます。ファクトリを活用すると特に短く書きやすくなります(ファクトリについては次章で説明します)。では firstname バリデーションのスペックについて詳細を見てみましょう。

spec/models/contact_spec.rb
1 # 名がなければ無効な状態であること
2 it "is invalid without a firstname" do
3   contact = Contact.new(firstname: nil)
4   contact.valid?
5   expect(contact.errors[:firstname]).to include("can't be blank")
6 end

今回は新しく作った連絡先( firstname には明示的に nil をセットします)が有効な状態にならず、連絡先の firstname 属性にエラーメッセージが付いていることを 期待(expect) します。RSpecをもう一度実行すると、二番目までのスペックがパスするはずです。

誤判定ではないことを証明するため、 tonot_to に変えてエクスペクテーションを反転させましょう。

spec/models/contact_spec.rb
1 # 名がなければ無効な状態であること
2 it "is invalid without a firstname" do
3   contact = Contact.new(firstname: nil)
4   contact.valid?
5   expect(contact.errors[:firstname]).not_to include("can't be blank")
6 end

当然のごとく、RSpecはテストの失敗を報告します。

Failures:

  1) Contact is invalid without a firstname
     Failure/Error: expect(contact.errors[:firstname]).not_to
         include("can't be blank")
       expected ["can't be blank"] not to include "can't be blank"
     # ./spec/models/contact_spec.rb:15:in `block (2 levels) in
         <top (required)>'

これはテストが正しく動作していることを確認する簡単な方法です。シンプルなバリデーションからもっと複雑なロジックに進むときであれば、特に有効です。先に進む前に not_toto に戻すことをお忘れなく。

同じやり方で :lastname のバリデーションもテストできます。

spec/models/contact_spec.rb
1 # 姓がなければ無効な状態であること
2 it "is invalid without a lastname" do
3   contact = Contact.new(lastname: nil)
4   contact.valid?
5   expect(contact.errors[:lastname]).to include("can't be blank")
6 end

「こんなテストは役に立たない。モデルに含まれるすべてのバリデーションを確認しようとしたらどれくらい大変になるのかわかっているのか?」そんなふうに思っている人もいるかもしれません。ですが、実際はあなたが考えている以上にバリデーションは書き忘れやすいものです。しかし、それよりもっと大事なことは、テストを書いている最中にモデルが持つべきバリデーションについて考えれば、バリデーションの追加を忘れにくくなるということです。(このプロセスはテスト駆動開発でコードを書くのが理想的ですし、最後は実際そうします。)

メールアドレスがユニークであることをテストする場合も大変シンプルです。

spec/models/contact_spec.rb
 1 # メールアドレスが重複する場合は無効な状態である
 2 it "is invalid with a duplicate email address" do
 3   Contact.create(
 4     firstname: 'Joe', lastname: 'Tester',
 5     email: 'tester@example.com'
 6   )
 7   contact = Contact.new(
 8     firstname: 'Jane', lastname: 'Tester',
 9     email: 'tester@example.com'
10   )
11   contact.valid?
12   expect(contact.errors[:email]).to include("has already been taken")
13 end

ここではちょっとした違いがあることに注意してください。このケースではテストの前に連絡先を保存しました( new の代わりに create を呼んでいます)。それから2件目の連絡先をテスト対象のオブジェクトとしてインスタンス化しました。もちろん、最初に保存された連絡先は有効な状態(姓と名がどちらもある)かつ、メールアドレスも設定されている必要があります。あとの章ではこのプロセスをもっと効率よく処理する方法を説明します。

ではもっと複雑なバリデーションをテストしてみましょう。たとえばユーザーは重複した電話番号を持ってはいけない、という要件を考えます。つまり、ユーザーの自宅、会社、携帯電話の電話番号がそれぞれユニークでなければいけない、ということです。あなたならこれをどうやってテストしますか?

Phone モデルのスペックに移って、次のようなexampleを書きます。

spec/models/phone_spec.rb
 1 require 'rails_helper'
 2 
 3 describe Phone do
 4   # 連絡先ごとに重複した電話番号を許可しないこと
 5   it "does not allow duplicate phone numbers per contact" do
 6     contact = Contact.create(
 7       firstname: 'Joe',
 8       lastname: 'Tester',
 9       email: 'joetester@example.com'
10     )
11     contact.phones.create(
12       phone_type: 'home',
13       phone: '785-555-1234'
14     )
15     mobile_phone = contact.phones.build(
16       phone_type: 'mobile',
17       phone: '785-555-1234'
18     )
19 
20     mobile_phone.valid?
21     expect(mobile_phone.errors[:phone]).to include('has already been taken')
22   end
23 
24   # 2件の連絡先で同じ電話番号を共有できること
25   it "allows two contacts to share a phone number" do
26     contact = Contact.create(
27       firstname: 'Joe',
28       lastname: 'Tester',
29       email: 'joetester@example.com'
30     )
31     contact.phones.create(
32       phone_type: 'home',
33       phone: '785-555-1234'
34     )
35     other_contact = Contact.new
36     other_phone = other_contact.phones.build(
37       phone_type: 'home',
38       phone: '785-555-1234'
39     )
40 
41     expect(other_phone).to be_valid
42   end
43 end

Contact モデルと Phone モデルはActive Recordの関連を持っているので、今回はその追加情報をコードに提供してやる必要があります。最初のexampleでは2件の電話番号が設定されている1件の連絡先を作成しました。二つ目のexampleでは2件の異なる連絡先に同じ電話番号を設定しています。注意してほしいのはどちらのexampleでも連絡先を create している、つまりデータベースに保存している点です。これはテストしようとしている電話番号に連絡先を設定するためです。

そして Phone モデルには次のようなバリデーションを設定してあります。

app/models/phone.rb
validates :phone, uniqueness: { scope: :contact_id }

よってこれらのスペックは問題なくパスします。

もちろん、バリデーションはスコープが一つしかないような単純なものばかりではなく、もっと複雑になる場合があります。もしかするとあなたは複雑な正規表現やカスタムバリデータを使っているかもしれません。こうしたバリデーションもテストする習慣を付けてください。正常系のパターンだけでなく、エラーが発生する条件もテストしましょう。たとえば、これまでに作ってきたexampleではオブジェクトが nil で初期化された場合の実行結果もテストしました。

インスタンスメソッドをテストする

連絡先の姓と名を毎回連結して新しい文字列を作るより、 @contact.name を呼び出すだけでフルネームが出力されるようにした方が便利です。というわけでこんなメソッドが Contact クラスに作ってあります。

app/models/contact.rb
1 def name
2   [firstname, lastname].join(' ')
3 end

バリデーションのexampleと同じ基本的なテクニックでこの機能のexampleを作ることができます。

spec/models/contact_spec.rb
1 # 連絡先のフルネームを文字列として返すこと
2 it "returns a contact's full name as a string" do
3   contact = Contact.new(firstname: 'John', lastname: 'Doe',
4     email: 'johndoe@example.com')
5   expect(contact.name).to eq 'John Doe'
6 end

テストデータを作り、それからあなたが期待する振る舞いをRSpecに教えてあげてください。簡単ですね。では続けましょう。

クラスメソッドとスコープをテストする

では、 Contact モデルの機能をテストしましょう。テストするのは指定された一文字で名前が始まる連絡先を返す機能です。例えば、私が S をクリックすると SmithSumner は返ってくるが、Jones は返ってこない、といった感じです。実装方法はたくさんありますが、あくまでデモンストレーションが目的なので一つだけお見せします。

Contact モデルは次のようなシンプルなメソッドでこの機能を実装しています。

app/models/contact.rb
1 def self.by_letter(letter)
2   where("lastname LIKE ?", "#{letter}%").order(:lastname)
3 end

この機能をテストするため、 Contact のスペックに次のようなexampleを追加しましょう。

spec/models/contact_spec.rb
 1 require 'rails_helper'
 2 
 3 describe Contact do
 4 
 5   # 前に書いたバリデーションのexampleは省略...
 6 
 7   # マッチした結果をソート済みの配列として返すこと
 8   it "returns a sorted array of results that match" do
 9     smith = Contact.create(
10       firstname: 'John',
11       lastname: 'Smith',
12       email: 'jsmith@example.com'
13     )
14     jones = Contact.create(
15       firstname: 'Tim',
16       lastname: 'Jones',
17       email: 'tjones@example.com'
18     )
19     johnson = Contact.create(
20       firstname: 'John',
21       lastname: 'Johnson',
22       email: 'jjohnson@example.com'
23     )
24     expect(Contact.by_letter("J")).to eq [johnson, jones]
25   end
26 end

クエリの結果とソートの順番を両方テストしている点に注意してください。 jones が先にデータベースから取得されるはずですが、姓(lastname)でソートしているのでクエリの結果としては johnson が先に取得されます。

失敗をテストする

正常系のテストは終わりました。ユーザーが名前を選ぶと結果が返ってきます。しかし、結果が返ってこない文字を選んだときはどうでしょうか?そんな場合もテストした方が良いです。次のスペックがそのテストになります。

spec/models/contact_spec.rb
 1 require 'rails_helper'
 2 
 3 describe Contact do
 4 
 5   # バリデーションのexampleが並ぶ...
 6 
 7   # マッチしなかったものは結果に含まれないこと
 8   it "omits results that do not match" do
 9     smith = Contact.create(
10       firstname: 'John',
11       lastname: 'Smith',
12       email: 'jsmith@example.com'
13     )
14     jones = Contact.create(
15       firstname: 'Tim',
16       lastname: 'Jones',
17       email: 'tjones@example.com'
18     )
19     johnson = Contact.create(
20       firstname: 'John',
21       lastname: 'Johnson',
22       email: 'jjohnson@example.com'
23     )
24     expect(Contact.by_letter("J")).not_to include smith
25   end
26 end

このスペックではRSpecの include マッチャを使って Contact.by_letter("J") で返ってきた配列を検証しています。そしてパスしました!これで理想的な結果、すなわちユーザーが結果が返ってくる文字列を選んだ場合だけでなく、結果が返ってこない文字を選んだ場合もテストしたことになります。

マッチャについてもっと詳しく

これまで三つのマッチャを実際に使いながら見てきました。最初に使ったのは be_valid です。このマッチャは rspec-rails gemが提供するマッチャで、Railsのモデルの有効性をテストします。 eqincluderspec-expectations で定義されているマッチャで、前章でRSpecをセットアップしたときに rspec-rails と一緒にインストールされました。

RSpecが提供するデフォルトのマッチャをすべて見たい場合はGitHubにあるrspec-expectationsリポジトリのREADMEが参考になるかもしれません。また、第7章では自分でカスタムマッチャを作る方法も説明します。

describe、context、before、afterを使ってスペックをDRYにする

もしあなたがここまでサンプルコードを見てきたのであれば、ここで説明した内容との食い違いを見つけているはずです。サンプルコードではまた別のRSpecの機能を使っています。それは before フックです。 before フックを使うとスペックのコードがシンプルになり、タイプ量も減らせます。実際、スペックのexampleにはいくつか冗長な部分があります。たとえば各exampleで全く同じ三つのオブジェクトを作っているところです。アプリケーションコードと同様に、DRY原則はテストコードにも当てはまります(いくつか例外もあるので、のちほど説明します)。ではRSpecのトリックを使ってテストコードをきれいにしてみましょう。

最初にやることは describe Contact ブロックの 中に describe ブロックを作ることです。これはフィルタ機能にフォーカスするためです。アウトラインを抜き出すと、このようになります。

spec/models/contact_spec.rb
 1 require 'rails_helper'
 2 
 3 describe Contact do
 4 
 5   # バリデーションのexampleが並ぶ...
 6 
 7   # 文字で姓をフィルタする
 8   describe "filter last name by letter" do
 9     # フィルタのexampleが並ぶ...
10   end
11 end

二つの context ブロックを加えてさらにexampleを切り分けましょう。 context の一つは文字にマッチする場合で、もう一つは文字にマッチしない場合です。

spec/models/contact_spec.rb
 1 require 'rails_helper'
 2 
 3 describe Contact do
 4 
 5   # バリデーションのexampleが並ぶ...
 6 
 7   # 文字で姓をフィルタする
 8   describe "filter last name by letter" do
 9     # マッチする文字の場合
10     context "matching letters" do
11       # マッチする場合のexampleが並ぶ...
12     end
13 
14     # マッチしない文字の場合
15     context "non-matching letters" do
16       # マッチしない場合のexampleが並ぶ...
17     end
18   end
19 end

気付いたかもしれませんが、このようにexampleのアウトラインを作ると、同じようなexampleをひとまとめにして分類できます。こうするとスペックがさらに読みやすくなります。では最後に、 before フックを利用してスペックのリファクタリングを完了させましょう。

spec/models/contact_spec.rb
 1 require 'rails_helper'
 2 
 3 describe Contact do
 4 
 5   # バリデーションのexampleが並ぶ...
 6 
 7   # 文字で姓をフィルタする
 8   describe "filter last name by letter" do
 9     before :each do
10       @smith = Contact.create(
11         firstname: 'John',
12         lastname: 'Smith',
13         email: 'jsmith@example.com'
14       )
15       @jones = Contact.create(
16         firstname: 'Tim',
17         lastname: 'Jones',
18         email: 'tjones@example.com'
19       )
20       @johnson = Contact.create(
21         firstname: 'John',
22         lastname: 'Johnson',
23         email: 'jjohnson@example.com'
24       )
25     end
26 
27     # マッチする文字の場合
28     context "matching letters" do
29       # マッチする場合のexampleが並ぶ...
30     end
31 
32     # マッチしない文字の場合
33     context "non-matching letters" do
34       # マッチしない場合のexampleが並ぶ...
35     end
36   end
37 end

RSpecの before フックはスペック内の汚い重複コードをきれいにするために必要不可欠な機能です。ご想像の通り、 before ブロックの中にあるコードは、 describe ブロックの中にある exampleの前(before)に実行されます。ただし、 describe ブロックの外にあるexampleは対象外となります。このケースでは describe ブロックの中にある exampleの前に、 before フックを実行させるようにしたので、RSpecはexampleを実行するたびに別個のテストデータを作ってくれるのです。また、 before フックは describe "filter last name by letter" ブロックの中で のみ 呼ばれます。つまり、元々あったバリデーションのスペックからは @smith@jones@johnson にアクセスできません。

また、三つの連絡先(テストデータ)で注意したいのは、これらの連絡先は各exampleの内部では作成されないので、インスタンス変数にセットしなければならないという点です。インスタンス変数に設定しているからこそ、 before ブロックの外側、すなわち各exampleの内部で作成した連絡先にアクセスできるのです。

もしexampleの実行後に後片付けが必要になるのであれば(たとえば外部サービスとの接続を切断する場合など)、 after フックを使って各exampleのあと(after)に後片付けすることもできます。RSpecの場合、デフォルトでデータベースの後片付けをやってくれるので after を使うことは滅多にありません。しかし、 before フックはなくてはならないものです。

さて、整理後の全スペックを見てみましょう。

spec/models/contact_spec.rb
 1 require 'rails_helper'
 2 
 3 describe Contact do
 4   # 姓と名とメールがあれば有効な状態であること
 5   it "is valid with a firstname, lastname and email" do
 6     contact = Contact.new(
 7       firstname: 'Aaron',
 8       lastname: 'Sumner',
 9       email: 'tester@example.com')
10     expect(contact).to be_valid
11   end
12 
13   # 名がなければ無効な状態であること
14   it "is invalid without a firstname" do
15     contact = Contact.new(firstname: nil)
16     contact.valid?
17     expect(contact.errors[:firstname]).to include("can't be blank")
18   end
19 
20   # 姓がなければ無効な状態であること
21   it "is invalid without a lastname" do
22     contact = Contact.new(lastname: nil)
23     contact.valid?
24     expect(contact.errors[:lastname]).to include("can't be blank")
25   end
26 
27   # メールアドレスがなければ無効な状態であること
28   it "is invalid without an email address" do
29     contact = Contact.new(email: nil)
30     contact.valid?
31     expect(contact.errors[:email]).to include("can't be blank")
32   end
33 
34   # 重複したメールアドレスなら無効な状態であること
35   it "is invalid with a duplicate email address" do
36     Contact.create(
37       firstname: 'Joe', lastname: 'Tester',
38       email: 'tester@example.com'
39     )
40     contact = Contact.new(
41       firstname: 'Jane', lastname: 'Tester',
42       email: 'tester@example.com'
43     )
44     contact.valid?
45     expect(contact.errors[:email]).to include("has already been taken")
46   end
47 
48   # 連絡先のフルネームを文字列として返すこと
49   it "returns a contact's full name as a string" do
50     contact = Contact.new(
51       firstname: 'John',
52       lastname: 'Doe',
53       email: 'johndoe@example.com'
54     )
55     expect(contact.name).to eq 'John Doe'
56   end
57 
58   # 文字で姓をフィルタする
59   describe "filter last name by letter" do
60     before :each do
61       @smith = Contact.create(
62         firstname: 'John',
63         lastname: 'Smith',
64         email: 'jsmith@example.com'
65       )
66       @jones = Contact.create(
67         firstname: 'Tim',
68         lastname: 'Jones',
69         email: 'tjones@example.com'
70       )
71       @johnson = Contact.create(
72         firstname: 'John',
73         lastname: 'Johnson',
74         email: 'jjohnson@example.com'
75       )
76     end
77 
78     # マッチする文字の場合
79     context "with matching letters" do
80       # マッチした結果をソート済みの配列として返すこと
81       it "returns a sorted array of results that match" do
82         expect(Contact.by_letter("J")).to eq [@johnson, @jones]
83       end
84     end
85 
86     # マッチしない文字の場合
87     context "with non-matching letters" do
88       # マッチしなかったものは結果に含まれないこと
89       it "omits results that do not match" do
90         expect(Contact.by_letter("J")).not_to include @smith
91       end
92     end
93   end
94 end

これらのスペックを実行すると、こんなふうに素敵なアウトラインが表示されます(第2章でドキュメント形式を使うようにRSpecを設定したからです)。

Contact
  is valid with a firstname, lastname and email
  is invalid without a firstname
  is invalid without a lastname
  is invalid without an email address
  is invalid with a duplicate email address
  returns a contact's full name as a string
  filter last name by letter
    with matching letters
      returns a sorted array of results that match
    with non-matching letters
      omits results that do not match

Phone
  does not allow duplicate phone numbers per contact
  allows two contacts to share a phone number

Finished in 0.51654 seconds (files took 2.24 seconds to load)
10 examples, 0 failures
どれくらいDRYだとDRYすぎるのか?

本章では長い時間をかけてスペックを理解しやすいブロックに分けて整理しました。すでに述べたように、 before ブロックはスペックを整理する際に不可欠なテクニックです。しかし、これは弊害を起こしやすいテクニックでもあります。

exampleのテスト条件をセットアップする際、可読性を考えてDRY原則に違反するのは問題ありません。私はそう考えています。もし自分がテストしている内容を確認するために、大きなスペックファイルを頻繁にスクロールしているようなら(もしくはあとで説明する外部のサポートファイルを大量に読み込んでいるようなら)、テストデータのセットアップを小さな describe ブロックの中で重複させることを検討してください。 describe ブロックの中だけでなく、exampleの中でもOKです。

とはいえ、そんな場合でも変数とメソッドに良い名前を付けるのは大変効果的です。たとえば、上のスペックでは @jones@johnson をテスト用の連絡先として使いました。こうした名前は @user1@user2 と書くよりもずっと理解しやすいです。なぜならテスト用オブジェクトの値の一部をテストで使っているからです。ここでは頭文字検索機能が正しく動いているかどうかを確認するために、オブジェクトの値を使っているのでした。第6章で特定のロールを持ったユーザをテストするときには、 @admin_user@guest_user といった名前の変数を使うともっと良いです。変数には意味のある名前を付けましょう!

まとめ

本章では私がモデルをテストする方法にフォーカスしましたが、このあとに登場するモデル以外のスペックでも使えるその他の重要なテクニックもたくさん説明しました。

  • 期待する結果は能動形で明示的に記述すること。 exampleの結果がどうなるかを動詞を使って説明してください。チェックする結果はexample一つに付き一個だけにしてください。
  • 起きて ほしい ことと、起きて ほしくない ことをテストすること。 exampleを書くときは両方のパスを考え、その考えに沿ってテストを書いてください。
  • 境界値テストをすること。 もしパスワードのバリデーションが4文字以上10文字以下なら、8文字のパスワードをテストしただけで満足しないでください。4文字と10文字、そして3文字と11文字もテストするのが良いテストケースです。(もちろん、なぜそんなに短いパスワードを許容し、なぜそれ以上長いパスワードを許容しないのか、と自問するチャンスかもしれません。テストはアプリケーションの要件とコードを熟考するための良い機会でもあります。)
  • 可読性を上げるためにスペックを整理すること。 describecontext はよく似たexampleを分類してアウトライン化します。 before ブロックと after ブロックは重複を取り除きます。しかし、テストの場合はDRYであることよりも読みやすいことの方が重要です。もし頻繁にスペックファイルをスクロールしていることに気付いたら、それはちょっとぐらいリピートしても問題ないというサインです。

アプリケーションに堅牢なモデルスペックを揃えたので、あなたは順調にコードの信頼性を上げてきています。次章ではここで説明したテクニックをさらに深く掘り下げて、アプリケーションコントローラへつなげていきます。

Q&A

describeとcontextはどう使い分けるべきでしょうか? RSpecの立場からすれば、あなたはいつでも好きなときに describe が使えます。RSpecの他の機能と同じく、 context はあなたのスペックを読みやすくするためにあります。私が本章でやったように、一つの条件をまとめるために context を使うのも良いですし、アプリケーションのある状態をまとめるために context を使うこともできます。

演習問題

ここまで私たちは、自分が書いたスペックは誤判定していない、という仮定のもとで話を進めてきました。スペックはどれも途中でも失敗することなく、保留中の状態からパスしている状態に変わりました。次のようなことをやってみて、スペックが本当に有効かどうかをチェックしてください。

  • テスト中のアプリケーションコードをコメントアウトしてみる。 たとえば、連絡先のスペックでは名の必須バリデーションを検証するexampleがあります。そこで、 validates :firstname, presence: true をコメントアウトしてからスペックを実行し、 it "is invalid without a firstname" が失敗するのを確認してみましょう。それからコメントを元に戻して、スペックがもう一度パスするのを確認してください。
  • エクスペクテーションの中で create メソッドに渡されるパラメータを変更してみる。 今度は it "is invalid without a firstname" を変更して、 :firstnamenil 以外の値を渡してみましょう。スペックは失敗するはずです。それからまた nil に戻して、スペックが再度パスすることを確認してください。

訳者あとがき

伊藤淳一

RailsもRSpecも、日本では比較的ポピュラーなwebフレームワーク/テストツールです。しかし、日本語で書かれたRailsの技術書やRSpecの技術書は発売されていても、「RSpecでRailsをテストする」というテーマだけにフォーカスを当てた日本の技術書はおそらくないと思います。きっと、日本の多くの技術者はweb上に散らばった情報を参考にしたり、職場のメンバーに教えてもらったりしながら、各自で「RSpecでRailsをテストする方法」を模索し続けていたのではないでしょうか。実際、私がそうでしたから。

本書、 Everyday Rails - RSpecによるRailsテスト入門 (原題: Everyday Rails Testing with RSpec )はそんな日本のRailsプログラマの状況をきっと変えてくれる一冊になると私は信じています。非常に初歩的な話から中級者でも知らないような高度なテクニックまで、これほど体系立てて実践的に説明してくれる技術書は他にないからです。本書を読んで内容を理解し、Aaronが言うとおりに自分のアプリケーションで自動テストを組みこんで練習すれば、全くのRSpec初心者でも一気に自動テストのスキルを向上させることができるはずです。目を使って 読む だけではなく、ぜひ自分の手と頭を動かして本書の内容を 身体で理解 してください!

最後に、私の家族へ向けて感謝の気持ちを。スーパーマンではなく、ただの凡人である私は、翻訳の作業時間を作るために家族との時間を削ることぐらいしかできませんでした。妻にも子どもたちにも、ここ数ヶ月はちょっと淋しい思いをさせていたかもしれません。今まで我慢してくれてどうもありがとう。この翻訳の仕事が落ち着いたら、みんなでどこかのんびりと旅行にでも行きましょう!

秋元利春

Rails & RSpecの学習に今から取り組む方にはぜひ読んでほしいと思える一冊に仕上がりました。翻訳チームのメンバーも本書でRSpecを学んでおり、非常に良い学習体験を与えてくれる一冊だと思います。そんな本書の日本語版翻訳に携わることができて本当に嬉しいです。Aaronさん素晴らしい本をありがとう。 そしてこの本を手に取ってくれたあなたに感謝します。あなたに楽しいRuby Lifeが訪れますように。

翻訳作業は想像していた以上に大変な作業でした。翻訳を終えてから過去の技術書を振りかえると、読みやすく伝わりやすい文体で私達の元へ届ける努力をしてくれていた翻訳者の方々の苦労がわかり、彼らには本当に頭が上がらないなと感じました。

とはいえ、確かに大変な作業ではありましたが、得られるものも多かったです。技術的な学習はもちろん、より深く文章の意図やニュアンス、文体のリズムに想いを馳せるきっかけになりました。本当に貴重な経験になりました。

正式版リリース準備に追われて勉強会の最中に翻訳作業にいそしむ私を温かく見守ってくれただけでなく、翻訳に関する相談にも乗ってくれた神戸.rbメンバーのみんな、そして原文解釈に悩んだ際に助けてくれた西脇.rbのマイケルに感謝します。

この翻訳チームの皆さんと一緒に翻訳作業ができて良い経験になりました。翻訳作業自体も大変でしたが、それに付随する諸々のタスクに追われた時にはチームメンバーでサポートしながら問題なく翻訳作業を進められました。伊藤淳一さん、魚振江さんには本当に感謝しています。

最後に。本業や翻訳、コミュニティ活動で忙しくなる私を精神面、体調面で支えてくれている妻と愛猫に感謝します。いつもありがとう。

魚振江

今回、日本語版の翻訳チームに参加できて、そして無事にリリースできてすごく良かったです。これまでの道のりを振り返ってみると、私にとってこの翻訳プロジェクトはなかなか大変なものでした。

まず、私は本書の中国語版をすでに読んでいました。読んだ当時はまだ日本語版がなかったので、本書を日本語で翻訳したいとぼんやりと考えていました。その後、いつも拝読している伊藤さんのブログで英語版の紹介がたびたび出てきて、比較的大きな反響を得ているのを目にしました。そこで実際に翻訳してみようという気持ちになり、2013年10月上旬頃、著者のAaronさんに連絡して翻訳を開始しました。しかし、早くも第1章から長文和訳に苦戦し始め、一人では翻訳作業を思うように進められませんでした。2013年11月頃、伊藤さんに声を掛けて頂き、伊藤さんがプロジェクトをリードする形で翻訳を進めてきました。また、12月からAkiさんも参加してもらい、いろいろと助けていただきました。

このように振り返ってみると、伊藤さんとAkiさんの助けなしでは日本語版の翻訳は全くできなかったと思います。お二人には大変感謝しています。そして、私たちの手で本書の翻訳を完了させたことが、少しでも読者のみなさんのお役に立てば嬉しいです。

日本語版の謝辞

本書を翻訳するにあたって、お世話になった方々のお名前を挙げさせてください。翻訳者チームの力だけでは本書の翻訳を完成させることは決してできませんでした。

まず、著者のAaronとはFacebookグループやGitHubのIssue上で何度もやりとりを交わしました。忙しい中、毎回丁寧に応対してくれたことを非常に感謝しています。ちなみに、Aaronのラストネームは「サマー(Summer)」ではありません。「サムナー(Sumner)」ですのでお間違いなく。

技術評論社の傳智之さんには技術書を出版する際の進め方についてアドバイスをいただきました。楽天株式会社の藤原大さんには翻訳者としての経験を元に貴重なアドバイスをいただきました。

Leanpubは海外発のサービスということもあって、時々電子書籍中の日本語表示がおかしくなることがありました。そんなときに何度も辛抱強く我々の修正リクエストに応対してくれたLeanpubのMike, Scott, Peterにも感謝しています。おかげでとてもきれいな日本語の電子書籍が完成しました。また、電子書籍で使えそうな日本語フォントを探しているときにTwitterで「あおぞら明朝」の存在を教えてくれたがんじゃさんと、このフォントを作られたbluskisさんにも大変感謝しています。

お忙しい中、ベータ版のレビューをしてくれた橋立友宏さん西川茂伸さん遠藤大介さんにも非常に助けられました。どなたも我々だけでは気付かなかった翻訳の問題点や技術的な誤りを指摘していただきました。そして、西脇.rbのイギリス人プログラマ、マイケル(P. Michael Holland)はほとんど4人目の翻訳者と呼んでも差し支えないぐらいの活躍をしてもらいました。もしマイケルが英語と日本語の橋渡しをしてくれていなければ、この翻訳がもっともっと辛い作業になっていたことは間違いありません。

最後に、本書を購入してくださったみなさんに感謝します。本書のベータ版を発売する前は、こんなにたくさんの方が本書を購入して下さるとは思いませんでした。翻訳者一同、本当に感謝しています。また、「本書を読んだおかげでRailsのテスト力が上がった」なんていう声があちこちから聞こえてくることを楽しみにしています。ぜひ、ご自身のTwitterやブログ等で感想を聞かせて下さい。ご意見やご質問でも構いません。本書は引き続きバージョンアップを繰り返していく予定です。「読み終わったからこれでおしまい」ではなく、今後もまたみなさんと紙面で(画面で?)再会できることを楽しみにしています。ではそのときまで、ごきげんよう。

 

翻訳者一同

Everyday Railsについて

Everyday Rails はRuby on Railsに関するTipsやアイデアを紹介するブログです。あなたのアプリケーション開発に役立つ素晴らしいgemやテクニック等も紹介しています。Everyday RailsのURLはこちらです。http://everydayrails.com/

著者について

Aaron Sumner はDjangoの国の中心部に住んでいるRuby開発者です(訳注: DjangoはPythonで実装されたwebフレームワーク。筆者の住むカンザス州で生まれた)。

彼は1990年代の半ばからwebアプリケーションを開発しています。その間彼はCGIをAppleScriptで(本気です)、Perlで、PHPで、そしてRubyとRailsで作ってきました。仕事を終えてテキストエディタの前から離れると、Aaronは写真や野球(Cardinalsを応援しています)、カレッジバスケットボール(カンザス大学Jayhawksのファンです)、アウトドアクッキング、木工制作、ボーリングなどを楽しんでいます。彼は妻のEliseと5匹の猫、それに1匹の犬と一緒に、カンザスの田舎に住んでいます。

Aaronの個人ブログは http://www.aaronsumner.com/ です。 Everyday Rails - RSpecによるRails テスト入門 (原題: Everyday Rails Testing with RSpec )は彼が書いた最初の本です。

訳者紹介

伊藤淳一

株式会社ソニックガーデンに勤務するRailsプログラマ。Rubyコミュニティ西脇.rbの主催者として神戸近辺でRuby勉強会も開催している。個人ブログgive IT a tryで技術情報や日常のよもやま話等も発信中。Twitterアカウントは@jnchito

秋元利春

神戸・京都で活動するフリーランスRailsプログラマ。Rubyへの恩返しも兼ねてコミュニティ活動に勤しむ。神戸で神戸.rbKobe Rubyist Meetupを主催。RailsGirls Kyotoのorganizer, coachも務める。Twitterアカウントは@spring_aki

魚振江

Railsプログラマ。Yokohama.rbと東京近くのRuby/Railsコミュニティで活動しています。Twitterアカウントは@blueplanet42

カバーの説明

カバーで使用した実用的で信頼性の高そうな赤いピックアップトラックの写真はiStockphotoの投稿者であるHabman_18によって撮影されたものです。私は長い時間をかけて(もしかすると長すぎたかも)カバー用の写真を探しました。私がこの写真を選んだ理由は、この写真がRailsのテストに対する私の姿勢を表していると思ったからです。つまり、どちらも派手ではなく、目的地に到達する最速の手段になるとは限りませんが、頑丈で頼りになります。そして、この車はRubyのような赤色です。いや、もしかすると緑色の方が良かったかもしれません。スペックがパスするときの色みたいに。むむむ。

変更履歴

2017/05/05

  • 第5章の「整理」で提示したコードを実装コードと一致するように修正。

2017/04/29

  • 第4章と第5章にあった誤記を修正。

2017/03/21

  • 「追加コンテンツ「RSpecユーザのためのMinitestチュートリアル」について」にあったtypoを修正。

2016/10/10

  • 第5章の細かい文章表現を改善。

2016/06/08

  • 第10章にあったtypoを修正。

2015/09/15

  • 第9章にあったtypoを修正。

2015/06/30

  • 追加コンテンツ「RSpecユーザのためのMinitestチュートリアル」に関する説明を追加。

2014/12/29

  • 原著の2014-12-19版に追従。
    • binstubを使用するサンプルコードの修正
    • DatabaseCleanerに関する説明をアップデート(第8章)
    • タグに関する情報を追加(第9章)
    • pending から skip への変更とその理由の説明(第9章)
    • ファイルアップロードのサンプルコードを修正(第10章)
    • APIのテストで have_http_status マッチャを使うように変更(第10章)
    • rails scaffold を使用する際に、不要なassetsやヘルパーを作成しない方法を説明(第11章)
    • 情報源リストのアップデート
    • その他、細かい文章の改善

2014/11/23

  • 第3章にあった軽微な翻訳ミスを修正。

2014/10/24

  • RSpec 3.xとRails 4.1に対応したメジャーアップデート版を公開。
  • 第10章に外部サービスのテスト、APIのテスト等を加筆.
  • RSpec 2.99に関する章を削除。(必要であれば一つ前の版をダウンロードしてください。)
  • その他、細かい情報のアップデートや表現の修正等を実施。

2014/07/17

  • iPad版のKindleで表示したときに、鍵マークやエクスクラメーションマークのアイコンが大きく表示される問題を修正。

2014/05/22

  • 第12章「RSpec 3に向けて」を新しく追加。

2014/04/22

  • 第4章にあった軽微な誤字を修正。

2014/04/17

  • 第5章にあった軽微な誤字を修正。

2014/02/28

  • 正式版第1版作成。
  • 「サンプル」の訳を「example」に変更。
  • 「共有サンプル」の訳を「shared examples」に変更。
  • 「テストの主語」となっていた箇所を「テストの対象」に変更。
  • 訳者あとがき、日本語版の謝辞、および訳者紹介のページを追加。
  • 翻訳全体に関して、日本語としての読みやすさを改善。
  • 誤字脱字、表記の揺れ、フォーマット崩れ、段落先頭の字下げの文字数、シンタックスハイライトの不一致等を修正。
  • 原著に合わせる形でサンプルコードに行番号を表示(表示されていない部分は原著通り)。
  • 原著の2014-02-24版に追従。
    • selenium-webdriver gemのバージョンを 2.35.1 から 2.39 に変更(第2章)。
    • 場所の重要性が本書の後半で重要になる理由を追記(第3章)。
    • eqinclude が定義されているgemに関する情報の誤りを修正(第3章)。
    • before メソッドの初期値に関する説明を追加(第3章)。
    • /contacts/:contact_id/phones/:id のパスが phone になっていたミスを修正(第5章)。
    • カスタムマッチャの定義例に関するリンクを変更(第7章)。
    • Seleniumの依存関係に関する説明を追記(第8章)。
    • Guardを使ったCSSコンパイルがSassやLESSを指していることを追記(第9章)。
    • モックのサンプルコードで before ブロックを2回記述してたミスを修正(第9章)。
    • 「古い習慣に戻らないでください!」のセクションで抜けていた後半部分の記述を追加(第12章)。
    • Relishが古いバージョンのRSpecをサポートしなくなったため、「RelishのRSpecドキュメント」のセクションにあった後半の記述を削除(Railsのテストの関するさらなる情報源)。
    • いくつかのサンプルコードにおいて小規模なリファクタリングを実施。

2014/02/10

  • rspec-rails のtypoを修正(第2章)。
  • PDFで見た場合に、一部の文章がページの外にはみ出してしまう問題を修正(第2章、およびRailsのテストの関するさらなる情報源)。

2014/02/07

  • ベータ版第1版作成(原著の2013/10/07版を翻訳)。