エンジニア備忘録

エンジニアを目指してる人のブログ

「EFFECTIVE TESTING WITH RSPEC 3: BUILD RUBY APPS WITH CONFIDENCE」の4、5章を読んだ

あとから自分で読み直せるように、ブログにまとめつつ読んでいます。

概要(自分の感想)

BDD(振る舞い駆動開発)を体験してみよう!(TDDかもしれない?)というコーナー。 RSpecならではのテスト・開発手法を擬似的に体験できるという感じ。 難易度は上がったけど、適度に新しい知識が入ってきてよき。

4章 アウトサイドに取り掛かろう:振る舞いテスト(Starting On The Outside: Acceptance Specs)

最初の一歩(First Steps)

新しいアプリケーションを作成し、テストについて学ぼう。

今回は家計簿アプリ(出費追跡アプリ)の作成を例にする。

アプリケーションが担う主な部分を以下に上げてみる。

  • Sinatraで書かれたアプリケーションは、(新たな支出を登録するためか、既に登録してある出費を探すために)HTTPリクエストを受け取る。

  • DB層はSequelを使用し、リクエスト間の出費を保存しておく。

  • Rubyのオブジェクトは出費を表現し、他の部分を接着する。

今回の一連の流れは、以下のようなものだ。

HTTPリクエスト → 出費ロジック(Rubyのコード) → アダプター(Sequel) → DB(SQLite

なぜRailsを使わない?(Why not Rails?)

Railsは今回のケースだと不必要なファイルを作成しすぎる。それに、今回作成するこういった小さなJSON APIsはSinatraの得意分野だ。

実際のコード

$bundle exec rspec --initしたら、まずspec_helper.rbの1行目にENV['RACK_ENV'] = 'test'と記入する。

これは、Sinatraはデフォルトだと例外を全て飲みこんで”500 Internal Server Error"レスポンスをレンダーするためだ。

次に、期待する振る舞いをテストコードとして書こう。

require 'rack/test'
require 'json'

module ExpenseTracker
  RSpec.describe 'Expense Tracker API' do
    include Rack::Test::Methods

    it 'records submitted expenses' do
      coffee = {
        'payee' => 'Starbucks',
        'amout' => '5.75',
        'date'  => '2017-06-10'
      }

      post '/expenses',
      Json.generate(coffee)
    end
  end
end

今回はBigDecimalというお金に関するGemを使わないけど、実際は検討したほうがいい。

Chap5 Testing in

振る舞いテストから単体テストへ(From Acceptance Specs to Unit Specs)

単体テストという言葉は、人によって使い方が違う。同じ人が使っていてもプロジェクトごとに違う意味になったりする。

今回のプロジェクトでは、「もっとも早く、もっとも疎結合なテスト」という意味合いで使おう。

基本的に、あらゆる層のテストは「その層のパブリックAPIによって動かされるべき」だ。そうしない方がより効果的な場合だと思う場合でなければ、このルールを守ったほうがいい。

バックトレースを削減したいときの設定

RSpec.configure do |config|
  config.filter_gems_from_backtrace 'rack', 'rack-test', 'sequel', 'sinatra'

gemがうむバックトレースを削減できる。削減されていないバックトレースがほしいなら-bオプションをつけよう。

依存性注入(Dependency Injection)

# こうではなく
class API < Sinatra::Base
  def initialize
    @ledger = Ledger.new
    super() #rest of initialization From sinatra
  end
end

# こう書くのが依存性注入
class API < Sinatra::Base
  def initialize(ledger: Ledger.new)
    @ledger = ledger
    super() #rest of initialization From sinatra
  end
end

# のちのちこうやって呼び出そう
app = API.new(ledger: Ledger.new)

理由

  • 依存性がinitializeで明確になっている
  • テストしやすいコードになる
  • ライブラリを他のプロジェクトで流用しやすい
  • コードそれ自体が判断根拠になる(コメント不要)

テストダブルを作る

RSpecではinstance_dobuleメソッドによって作成可能だ。

require_relative '../../../app/api'
require 'rack/test'

module ExpenseTracker
  RecordResult = Struct.new(:suceess?, :expense_id, :error_message)
  RSpec.describe API do
    include Rack::Test::Methods

    def app
      API.new(ledger: ledger)
    end

    let(:ledger) { instance_double('ExpenseTracker::Ledger')}

    describe 'POST /expenses' do
      context 'when the expense is successfully recorded' do
        it 'returns the expense id' do
          #このexpenseハッシュの値は重要でない。大事なのは「成功」か「失敗」レスポンスを得ることだ。
          expense = { 'some' => 'data' }

          #ledger、つまりExpenseTracker::Ledgerのインスタンスダブルに、recordメソッドを引数expenseを受け取らせる
          #そして、返り値にRecordResultのインスタンスを手に入れる
          allow(ledger).to receive(:record).with(expense)
            .and_return(RecordResult.new(true, 417, nil))

          #expenseをJSON形式にしたものを送る。これがappメソッドを呼び、そちらのクラスメソッドに処理が移る。
          post '/expenses', JSON.generate(expense)

          # 返ってきたJSONをパースしてRubyのハッシュにする。417を得られていたらテスト成功!
          parsed = JSON.parse(last_response.body)
          expect(parsed).to include('expense_id' => 417)
        end
        it 'responds with a 200(OK)'
      end

      context 'when the expense fails validation' do
        it 'returns an error message'
        it 'responds with a 422(Unprocessable entity)'
      end
    end
  end
end

成功をハンドルする (Handling success)

上記のスペックが通るための条件

  • リクエストボディからexpenseをパースする
  • Ledgerを使い、expenseを記録する(実際のDBか、テストのためのフェイクかどちらかであればいい)
  • idの情報を含んだJSONファイルを返す

コラム:テストをわざと失敗させる

自分が書いたテストが通ったあとは、一度失敗させてみるのがいい たとえば200を返してほしいテストでは404を返すようにアプリケーション側に記述を足してみる。 こうすることで、ある意味「テストのテスト」ができる。

コラム:成功してからリファクタリング(Refactor While Green)

冗長なコードを書いているとき、途中でリファクタリングしたくなるかもしれないが、出来る限り「テストが通ってから」リファクタリングを行おう。

コラム:セットアップコードとテストコードを分けよう(Keep Setup and Test Code Separate)

伝統的なテストの順序は、「セットアップ」、「振る舞い(act)」、「主張(assert)」だ。 beforeフックに記述するのは「セットアップコード」のみにしよう。DRYを目指しすぎて、テストコードまでbeforeフックに潜ませると、のちのちの新たなテストケースに対応できなくなる。

感想

オブジェクト指向実践設計ガイドに書いてた「テストダブル」について、RSpecでの実装方法が出てきてとても楽しい。

もしこの本だけ読んでたら、「テストダブル」の手法がRSpec流みたいなふうに思ってたと思うけど、逆の順番で取り組んでるから、テストダブルという概念に既にしっかりインデックス付けられている感覚があって読みやすかった。まあ、オブジェクト指向実践設計ガイド読んでるときは「ちょっと抽象的だなあ」という感覚があったから、このへんの、「体系知識→実装知識」の順で取り組むか、「実装知識→体系知識」の順で取り組むかは、難しいなあと思う。 ただ、極まってくると「体系知識→実装知識」の力は武器になるだろうな、という気がしている。(公式ドキュメントから振る舞いを予想できるエンジニアみたいな。)なので、体系知識をおいしく食べていく訓練はしたい!

話を戻して、4,5章でやっていることは、テストから書き始めるアプリケーション開発なんですが、噂に聞いていた手法を体験できてとても面白いです。 求める振る舞いベースからテストコードを始めるから、擬似的に「ベテラン開発者の思考の流れ」を追えるのもありがたい。技術のための実装じゃなくて、実装のための技術というアイデアがはっきり掴み取れる。

Amazonのレビューで評判良さげだからやり始めたけど、大正解だったかもしれない!!といってもまだ20%ぐらいしか終わってないわけですが。

2日で1チャプターのペースで続けられてるので、このまま続けます。