コモディティ化するエンジニア

組み込み系のSI屋から、Railsを扱うWeb系のベンチャーに転職した筆者が、日々ミジンコなりに情報を綴るブログ。

Rubyで写経:テスト駆動開発 第5章「原則をあえて破るとき」

テスト駆動開発という本を題材に、dodosoft のメンバーでもくもく写経会をやっています。
各々が違う言語で取り組んでいるので、言語ならではの悩み、言語共通のコンテキストを共有できて、なかなか学びがあります。

テスト駆動開発

テスト駆動開発

会場は ラクスル株式会社 の会議室です。いつも弊社ありがとうございます!


第5章 原則をあえて破るとき

TODOリスト

  • $5 + CHF = $10
  • $5 * 2 = $10
  • amount を privateにする
  • Dollarの副作用どうする?
  • Moneyの丸め処理どうする?
  • equals()
  • hashCode()
  • null との等値性比較
  • 他のオブジェクトとの等価性比較
  • 5 CHF * 2 = 10 CHF
  • Dollar と Franc の重複
  • equals の一般化
  • times の一般化

自分の気付き・感想

  • コピペのメンタルコスト

みんなで話した気付き・感想

  • 「Kent Beck が設計なんてどうでもいいって書いてたよ」
  • コードの重複排除は3つ目が出てきた時かなー
  • 今回のケースは、実際の実装を考えると、コピペじゃなくて、いきなり共通化しちゃう気がする
  • 実際のプロダクトコードを考えると、こんなクソコミット残したくないかも。。。
    • チームの理解が大切?
    • 恥ずかしさを捨てる?
    • squash marging を使えば、恥ずかしいのはコミットコメントだけになるので便利
      • ブランチの切り方統一すれば、featureごとにコミットがまとまるので、いいぞ!
      • ラクスルのハコベルプロジェクトではこのやり方をしている
  • t_wada さんの「クソコード」についての和訳バリエーションがすごい

実装面でのひっかかり

  • なし

以上です!
この章については何も言うことないですね。。。とりあえずコピペしたので、ここから重複を排除しつつテストも書いていくという話になるかと。
このクソコードがどう洗練されていくのか楽しみです!

次回は第6章です。

Rubyで写経:テスト駆動開発 第4章「意図を語るテスト」

テスト駆動開発という本を題材に、dodosoft のメンバーでもくもく写経会をやっています。
各々が違う言語で取り組んでいるので、言語ならではの悩み、言語共通のコンテキストを共有できて、なかなか学びがあります。

テスト駆動開発

テスト駆動開発

会場は ラクスル株式会社 の会議室です。いつも弊社ありがとうございます!


第4章 意図を語るテスト

TODOリスト

  • $5 + CHF = $10
  • $5 * 2 = $10
  • amount を privateにする
  • Dollarの副作用どうする?
  • Moneyの丸め処理どうする?
  • equals()
  • hashCode()
  • null との等値性比較
  • 他のオブジェクトとの等価性比較

自分の気付き・感想

  • 粗く実装してから結合度を下げていくのはなるほどな! と思った
    • 最初から細かく可視性まで考慮してしまい、結果的に手が遅くなることがよくあったなと思った
    • そんなぐずぐず悩んでいる暇があるのなら、テストを先に書いてしまって(インタフェースを先に決定づける)後からリファクタリングすればいいと思った
  • カプセル化はMUSTなのか、WANTなのか?

みんなで話した気付き・感想

  • TDD的には、いきなり完璧なコードを目指すのではなく、要求を実現するための最小限のコードを少しずつ書く。小さなステップを踏んでいくのだ!
    • 普段は最初からカプセル化を意識してコードを書いていくので、アプローチが逆方向だなと新鮮だった
  • 3章までにテストをしっかり書いていないと(テストが正しいこと)、この章で大胆には変更できなかった

実装面でのひっかかり

  • Rubyインスタンス変数のカプセル化。。。
  • eq だと同値性ではなく、同一性(object_idレベルで)を比較してしまうので、 == を使わなければならなかった

以上です!
はじめにインタフェースを設計できている必要があるので、テスト駆動開発ってスキルが必要だなー(今の自分には力不足。。。)と思いました。
ガチャガチャ実装しているうちに、インタフェース結構変えちゃうんですよね、自分の場合。がんばらねば。。。

次回は第5章です。

Rubyで写経:テスト駆動開発 第3章「三角測量」

テスト駆動開発という本を題材に、dodosoft のメンバーでもくもく写経会をやっています。
各々が違う言語で取り組んでいるので、言語ならではの悩み、言語共通のコンテキストを共有できて、なかなか学びがあります。

テスト駆動開発

テスト駆動開発

会場は ラクスル株式会社 の会議室です。いつも弊社ありがとうございます!


第3章 三角測量

第3章では、 equals という型と値が一致しているか確認するメソッドの実装をします。
Rubyは基本ダックタイピングなので、型を意識する機会がありませんが、
本章では返り値が Dollar 型であることを強く意識しています。
そのため、本書の意図にならって、Rubyでも型を意識したテストを書いています。

github.com

TODOリスト

  • $5 + CHF = $10
  • $5 * 2 = $10
  • amount を privateにする
  • Dollarの副作用どうする?
  • Moneyの丸め処理どうする?
  • equals()
  • hashCode()
  • null との等値性比較
  • 他のオブジェクトとの等価性比較

自分の気付き・感想

  • Rubyだと意識していないけれど、静的型付けの言語でテストを書く場合、型気にしないといけないんだね。。。
  • 他のメンバーは静的型付けで写経しているので、気にするポイントが違って面白い

他のメンバーの気付き・感想

  • 途中参加のメンバーは、参加の前に最低でもテスト回せてコンパイルができるプロジェクトを作っておいた方がいい
  • テストケースの書き方で、そのメソッドで意図していることを表現できるんですね。どんな条件でテストを書いているかで、その意図が見えてくる
  • テストするためのお膳立て(必要なオブジェクトの事前生成)は実際にはあるから、こんなに単純にはテスト書けないだろう(実際はクソ長くなる)

実装面でのひっかかり

  • it にラベルをつける時とつけない時の判断が自分の中でつかない....まあ、期待値を明示的にラベルしたい時は書いて置いた方がいいのかな?
    • たぶん、入力条件は context のラベルに書くべき

以上です!
型一致の評価を == としましたが、これ大丈夫なんですかね?(少なくとも eq はobject_idレベルで同一性比較をするので使えない)

次回は第4章です。サクサク進んで気持ち良いですね!

Rubyの Set / Array それぞれの検索速度を比較してみた

RubyではArrayよりSetの方が高速に検索(include?)できるとのことですが、実際どのぐらいの差があるか調べてみました。

実行環境

以下の通りです。

$ ruby -v          
ruby 2.3.1p112 (2016-04-26 revision 54768) [x86_64-darwin15]

計測に使うコード

require './process_measure.rb'
require 'set'

array = Array.new
set = Set.new
10001.times { |i| array.push("#{i}") }
10001.times { |i| set.add("#{i}") }

6.times do |i|
  str = "#{i * 2000}"
  puts "-------------------- #{str} --------------------"
  measure_do { array.include?(str) }
  measure_do { set.include?(str) }
end

process_measure.rbについては、以下の記事をご参照ください。

muramurasan.hatenablog.jp

計測結果

$ ruby set_array.rb
-------------------- 0 --------------------
      user     system      total        real
  0.000000   0.000000   0.000000 (  0.000007)
      user     system      total        real
  0.000000   0.000000   0.000000 (  0.000005)
-------------------- 2000 --------------------
      user     system      total        real
  0.000000   0.000000   0.000000 (  0.000071)
      user     system      total        real
  0.000000   0.000000   0.000000 (  0.000004)
-------------------- 4000 --------------------
      user     system      total        real
  0.000000   0.000000   0.000000 (  0.000115)
      user     system      total        real
  0.000000   0.000000   0.000000 (  0.000004)
-------------------- 6000 --------------------
      user     system      total        real
  0.000000   0.000000   0.000000 (  0.000170)
      user     system      total        real
  0.000000   0.000000   0.000000 (  0.000004)
-------------------- 8000 --------------------
      user     system      total        real
  0.000000   0.000000   0.000000 (  0.000395)
      user     system      total        real
  0.000000   0.000000   0.000000 (  0.000008)
-------------------- 10000 --------------------
      user     system      total        real
  0.000000   0.000000   0.000000 (  0.000576)
      user     system      total        real
  0.000000   0.000000   0.000000 (  0.000008)

考察

Setの方が明らかに高速ですね。 Arrayは先頭から順番に検索するんでしょうか? 検索対象が後ろになればなるほど遅くなる傾向にあります。

Arrayは便利なメソッドがたくさんありますが、追加/削除/検索しかせず、値が重複しないならばSetを使う方が良いですね。

Rails EngineでCapybaraを導入しようとしてハマったこと

Rails EngineにCapybaraを導入しようとしてハマったことが二つあるので、備忘録として残しておきたいと思います。

ルーティングを認識させる

Railsアプリを普通に書いている場合は気にする必要がないのですが、Rails Engineでfeature specをいきなり動かそうとすると、visit (path)のところで以下のようなエラーが発生します。

  1) method_call_situation pages (permit WebUI) render Succsess
     Failure/Error: visit method_call_situations_path
     
     NameError:
       undefined local variable or method `method_call_situations_path' for #<RSpec::ExampleGroups::MethodCallSituationPages::PermitWebUI:0x007fe1d21b1730>
     # ./spec/features/method_call_situations_page_spec.rb:34:in `block (2 levels) in <top (required)>'

Rails Engineではspec内でルーティングを認識させる必要があります。

...
RSpec.configure do |config|
  config.include OkuribitoRails::Engine.routes.url_helpers
end
...

これで、名前付きルートが認識されるようになります。

asset precompile を行うようにする

feature spec内でRails Engine内で持っている画像等のAssetを参照しようとすると、以下のようなエラーが発生します。

1) method_call_situation pages (permit WebUI) available search function
     Failure/Error: <%= image_tag "okuribito_rails/logo.png", class: "header-logo" %>
     
     ActionView::Template::Error:
       Asset was not declared to be precompiled in production.
       Add `Rails.application.config.assets.precompile += %w( okuribito_rails/logo.png )` to `config/initializers/assets.rb` and restart your server
     # ./app/views/layouts/okuribito_rails/_header.html.erb:2:in `___sers_ym_works_okuribito_rails_dev_app_views_layouts_okuribito_rails__header_html_erb__3728936773133030127_70218317871640'
     # ./app/views/layouts/okuribito_rails/application.html.erb:11:in `___sers_ym_works_okuribito_rails_dev_app_views_layouts_okuribito_rails_application_html_erb___1363027026573178022_70218307163340'
     # ./spec/features/method_call_situations_page_spec.rb:34:in `block (2 levels) in <top (required)>'
     # ------------------
     # --- Caused by: ---
     # Sprockets::Rails::Helper::AssetNotPrecompiled:
     #   Asset was not declared to be precompiled in production.
     #   Add `Rails.application.config.assets.precompile += %w( okuribito_rails/logo.png )` to `config/initializers/assets.rb` and restart your server
     #   ./app/views/layouts/okuribito_rails/_header.html.erb:2:in `___sers_ym_works_okuribito_rails_dev_app_views_layouts_okuribito_rails__header_html_erb__3728936773133030127_70218317871640'

Rails Engine単体では、asset precompileを行うように書かれていないハズなので、当たり前っちゃ当たり前です。 そこで、以下のようにengine.rbにて、asset precompileを行う設定にします。

module OkuribitoRails
  class Engine < ::Rails::Engine
    isolate_namespace OkuribitoRails

    initializer "okuribito_rails.assets.precompile", group: :all do |app|
      app.config.assets.precompile += %w(
        okuribito_rails/logo.png
      )
    end
  end
end

これで、テストが通るようになります。

なお、この対処方法が正しいかどうかは残念ながら保証しかねますが、rails_adminというgemのやり方を参考にしています。