2014-09-21 RSpecでPower Assertをやるには


RubyKaigi 2014でpower assertの話を聞いてrspecでどうにかならんかちょっと考えてみました。まず結論だけ書くとrspecでpower assertを使いたければ以下の様に書けばOK。

require 'rspec'
require 'minitest'
require 'minitest-power_assert'

module Minitest
  module Assertions
    prepend  Minitest::PowerAssert::Assertions
  end
end

RSpec.configure do |config|
  config.expect_with :minitest
end

describe 'Test' do
  it 'test' do
    assert { 1.to_s.class == 1.to_i.class }
  end
end

これを

$ rspec --color -rpower_assert power_assert.rb

で実行するとこんな感じ。power_assertは事前にrequireした方が情報量がちょっと増える。

Failures:

  1) Test test
     Failure/Error: assert { 1.to_s.class == 1.to_i.class }
     Minitest::Assertion:

           assert { 1.to_s.class == 1.to_i.class }
                      |    |     |    |    |
                      |    |     |    |    Fixnum
                      |    |     |    1
                      |    |     false
                      |    String
                      "1"

letとsubject

powerassertの0.1.3だとdefinedmethodで定義されたメソッドの値が取れていないらしく、letやsubjectの値が表示されない。現時点でのmasterの0.1.4devだと修正されているとのことなのでちゃんと表示される。

describe 'Test' do
  let(:klass) { 1.to_s.class }
  it 'test' do
    assert { klass == 1.to_i.class }
  end
end

これを実行すると

Failures:

  1) Test test
     Failure/Error: assert { klass == 1.to_i.class }
     Minitest::Assertion:

           assert { klass == 1.to_i.class }
                    |     |    |    |
                    |     |    |    Fixnum
                    |     |    1
                    |     false
                    String

こんな感じ。subjectも大体おなじ。

expectとマッチャ

この方法はMinitestのadapterとminitest-power_assertを使うようにしているので無理。

ちなみに rspec で 手軽に power_assert 出力できるようにする の方法でexpectを使ってみると

require 'rspec/core'
require 'power_assert'

module RSpec
  module PowerAssert
    def power_assert(&block)
      ::PowerAssert.start(block) do |pa|
        begin
          pa.yield
        rescue RSpec::Expectations::ExpectationNotMetError => e
          e.message << "\nPowerAssert:\n#{pa.message_proc.call}"
          raise e
        end
      end
    end
  end
end

class RSpec::Core::ExampleGroup
  include RSpec::PowerAssert
end

describe 'Test' do
  it 'test' do
    power_assert {
      expect(1.to_s.class).to eq(1.to_i.class)
    }
  end
end

これは

       PowerAssert:
             expect(1.to_s.class).to eq(1.to_i.class)
             |        |    |      |  |    |    |
             |        |    |      |  |    |    Fixnum
             |        |    |      |  |    1
             |        |    |      |  #<RSpec::Matchers::BuiltIn::Eq:0x007f0850c91d78 @expected=Fixnum, @actual=String>
             |        |    |      nil
             |        |    String
             |        "1"
             #<RSpec::Expectations::ExpectationTarget:0x007f0850caa170 @target=String>

こうなってしまう。この場合、expectの@targetにStringという結果が入っているのでそれを取り出すようにして、eqの方も@expectedに期待するものがはいってるのでそれを取りだすようにすればいいのかなぁ。

もしくはexpectとeqの中の値さえわかれば良いといえば良いのでいっそ値をださなくてもいいのかも? 例えばこんな感じ。

       PowerAssert:
             expect(1.to_s.class).to eq(1.to_i.class)
                      |    |              |    |
                      |    |              |    Fixnum
                      |    |              1
                      |    |
                      |    |
                      |    String
                     "1"


別の例としてbe_falseyだとこんな感じ。

     Failure/Error: expect(nil.to_s.to_i).to be_falsey
       expected: falsey value
            got: 0
       PowerAssert:
             expect(nil.to_s.to_i).to be_falsey
             |          |    |     |  |
             |          |    |     |  #<RSpec::Matchers::BuiltIn::BeFalsey:0x007fa84b53bb00 @actual=0>
             |          |    |     nil
             |          |    0
             |          ""
             #<RSpec::Expectations::ExpectationTarget:0x007fa84b3e02b0 @target=0>

これに関していうとbefalseyにはfalseが欲しいという情報がない。befalseyを見れば求めてるものはわかるって話かもしれないけど… 更に言うとRSpec3でComposable Matcherが入ったりとか、以前からあるCustom Matcherとかがあったりして、それら全部対応するのは厳しいなーという感じ(そもそも対応できるのかもよくわからない…)

そもそもの話をするとそういったマッチャというか、たくさんあるassertion methodを使い分けしたくないからpower assertをつかうわけで別にマッチャとか使わなくていいのではという気持ちがある。

なのでexpectとかカスタムマッチャとかは(power assertを使う部分では)諦めてassertですませるのがよさそうかなと個人的には思う。

config.expect_with :minitest
config.expect_with :rspec

spechelper.rbにminitestもrspecも両方使うよう書けばpowerassertを使いたいところと、rspecを使いたいところで分けることができるのでどうしてもマッチャを使いたいところは素直にマッチャを書いてpower assertは諦めるしかない。

今あるテスト資産をそのまんまpower assert対応にはできないのが悲しいところではあるけれども、このassertを使う方法でもexpectation部分以外はrspecの機能そのまま使えるのでそこまで悪くはないかなと思う。

まだpower_assert gemの実装を理解できていないので、もしかしたらうまいことやれるかもしれないけどとりあえずここでギブアップ…

2014-09-05 Arelあれこれ


Model.arel_table を読みづらいと感じる

例えばこういうコード。

Post.joins(:comments).order(Comment.arel_table[:created_at].desc)

発行されるSQLはこんな感じ。

SELECT "posts".* FROM "posts" INNER JOIN "comments" ON "comments"."post_id" = "posts"."id"  ORDER BY "comments"."created_at" DESC

こちらの方が個人的には読みやすい。

Post.joins(:comments).order('comments.created_at desc')

問題もある

テーブル名を変えてしまったとき

仮にテーブル名がかわったら後者の場合はエラーになる。

SELECT "posts".* FROM "posts" INNER JOIN "new_comments" ON "new_comments"."post_id" = "posts"."id"  ORDER BY comments.created_at DESC
SQLite3::SQLException: no such column: comments.created_at: SELECT "posts".* FROM "posts" INNER JOIN "new_comments" ON "new_comments"."post_id" = "posts"."id"  ORDER BY comments.created_at D
ESC
ActiveRecord::StatementInvalid: SQLite3::SQLException: no such column: comments.created_at: SELECT "posts".* FROM "posts" INNER JOIN "new_comments" ON "new_comments"."post_id" = "posts"."id"
  ORDER BY comments.created_at DESC

前者はモデルの方でテーブル名を変更すれば動作する。

class Comment < ActiveRecord::Base
  self.table_name  :new_comments
end

scopeでmergeしたいとき

class Comment < ActiveRecord::Base
  scope :recent, -> { where('created_at > ?', 10.days.ago) }
end

Post.joins(:comments).merge(Comment.recent)

これだとCommentのcreated_atが解決できなくてコケる。

SELECT "posts".* FROM "posts" INNER JOIN "comments" ON "comments"."post_id" = "posts"."id" WHERE (created_at > '2014-08-26 09:16:45.106691')
SQLite3::SQLException: ambiguous column name: created_at: SELECT "posts".* FROM "posts" INNER JOIN "comments" ON "comments"."post_id" = "posts"."id" WHERE (created_at > '2014-08-26 09:16:45.
106691')
ActiveRecord::StatementInvalid: SQLite3::SQLException: ambiguous column name: created_at: SELECT "posts".* FROM "posts" INNER JOIN "comments" ON "comments"."post_id" = "posts"."id" WHERE (cr
eated_at > '2014-08-26 09:16:45.106691')

正しくはこう。

class Comment < ActiveRecord::Base
  scope :recent, -> { where(self.arel_table[:created_at].gt(10.days.ago)) }
end
SELECT "posts".* FROM "posts" INNER JOIN "comments" ON "comments"."post_id" = "posts"."id" WHERE ("comments"."created_at" > '2014-08-26 09:19:17.406299')

僕個人の結論

Model.arel_tableはできる限り隠したい(読みづらいから)。そしてmergeを使いたいケースではArelを使っておく。その変わりmergeが必要になるまでは無理にArelを使わない。

さっきのComment#recentの例でいうなら最初はwhere('created_at > ?', 10.days.ago)で書いておく。merged使いたい時になったらArelの方で書き直す。

テーブル名の変更のリスクは気にしない。そもそもあまりテーブル変更しないし…。既にあるDBの上にRailsアプリケーションを構築する場合はDBリファクタリングのことを考えてArel使いまくった方がいいのかもしれない。

2014-04-18 Vim(TagBar)でRSpecのctagsを扱う


rspec ctags

unite-outlineとかを使う人には不要なのかもしれないけど、あいにくuniteユーザではないのでctagsでなんとかできないか調べてみた。

まず、Funtooで入るctagsではrspecのタグは生成できないのでforkされたctagsを使う必要がある。

fishman/ctags

インストール場所はお好みで。個人的には手で入れる系のものは$HOME/localにインストールするのが好きなのでそこにインストールした。あとはTagBarの方でこのctagsを使うよう設定する。

let g:tagbar_ctags_bin="/home/ukstudio/local/bin/ctags"
let g:tagbar_type_ruby = {
    \ 'kinds' : [
        \ 'm:modules',
        \ 'c:classes',
        \ 'd:describes',
        \ 'C:contexts',
        \ 'f:methods',
        \ 'F:singleton methods'
    \ ]
\ }

これで上の画像のような感じでTagBarに表示されるようになるはず。

2014-03-27 lsと間違えてerutasoを打ってしまうGentoo/Funtooユーザーのみなさまへ


ebuildを作ってみましたのでご活用ください。初めてのebuildなので不具合・不都合あればpull-reqを。ukstudioというoverlayを作りましたのでそこからインストールできるはずです。

https://github.com/ukstudio/ukstudio-overlay

curl https://raw.github.com/ukstudio/ukstudio-overlay/master/profiles/layman.xml > /etc/layman/overlays/ukstudio-overlay.xml
layman -f -a ukstudio

emerge erutaso
which erutaso #=> /usr/bin/erutaso

erutaso

See also

https://twitter.com/sgymtic/status/448832543039574016

2014-03-18 BitlBeeでHipChatに接続する


HipChatのLinuxクライアントは残念ながら日本語入力ができないのでかわりにIRCを使ってみる。 HipChatはJabberが使えるのでBitlBeeを使ってJabber経由でIRCと繋ぐことにする。BitlBee自体の設定はなにもいらないので各環境にあわせて適当に入れて起動する。

sudo emerge bitlbee
sudo /etc/init.d/bitlbee start

次にIRCクライアントでBitlBeeに接続する。BitlBeeはlocalhostに6667ポートで立ち上がっているので(コンフィグで修正していなければ)、そこに接続する。文字コードはUTF-8でOK。 接続できたらbitlbeeの部屋でコマンドを入力し、アカウントを追加する。HipChatのJabberのアカウントは「Account Setting > XMPP/Jabber info」にある。

account add jabber username@chat.hipchat.com 'password'

次に各ユーザがニックネームで表示されるように設定する。これをやらないと数字の羅列で誰が誰だかわからない状態になってしまう。

account hipchat set nick_source full_name

次にアカウントを有効化する。この時点で認証などもしているので失敗したらアカウント情報を見直すこと。

account hipchat on

有効化できたらすでに部屋に参加できるが数字の羅列が頭についているので使いにくい。別の名前を割り当てることができるので適当に自分が使いやすい名前をつける。

chat add hipchat room_jaber_name@conf.hipchat.com #channelname

ニックの設定次第だとHipChatから怒られるのでその場合HipCHatで使っている名前を設定する。

channel #channelname set nick 'room nickname'

無事、部屋にjoinしてログが流れてきたら終了。

/join #channelname