Hatena::Grouprubyist

Going My Ruby Way このページをアンテナに追加 RSSフィード

Ruby ロゴ (C) Ruby Association LLC

2011年07月24日(日)

y

| 23:23 |   y - Going My Ruby Way を含むブックマーク はてなブックマーク -   y - Going My Ruby Way   y - Going My Ruby Way のブックマークコメント

require 'yaml' すると Kernel#.y が使える。

y はオブジェクトをYAMLな感じで表示してくれる。

$ irb -r yaml
>> h = {:a => 10,:b => 20}
=> {:a=>10, :b=>20}
>> y h
--- 
:a: 10
:b: 20
=> nil

ということで、~/.irbrc に追加しました。

require 'irb/completion'
require 'pp'
require 'yaml'   # 追加!

IRB.conf[:PROMPT_MODE] = :SIMPLE

-----

もしやと思って -r json してみたら j が使えました。(知らないことばっかしや)

$ irb -r json
>> h = {:a => 10,:b => 20}
=> {:a=>10, :b=>20}
>> j h
{"a":10,"b":20}
=> nil

こっちは、.irbrc には入れなくていいか。

gem の実行

| 23:04 |  gem の実行 - Going My Ruby Way を含むブックマーク はてなブックマーク -  gem の実行 - Going My Ruby Way  gem の実行 - Going My Ruby Way のブックマークコメント

例。

$ s gem1.9.1 install --remote enumerable_lz

(UbuntuRuby は 1.8 がデフォルトです。いろいろ混乱します)

----

ちなみに、s は sudo のエイリアスです。

$ a s
alias s='sudo'

で、a は alias のエイリアスです。

.vimrc に書くこと

| 22:49 |  .vimrc に書くこと - Going My Ruby Way を含むブックマーク はてなブックマーク -  .vimrc に書くこと - Going My Ruby Way  .vimrc に書くこと - Going My Ruby Way のブックマークコメント

~/.vimrc に以下を追加。*.rb ファイルの判別して時 sw=2 等の設定をする。

filetype on
au FileType ruby set ts=2 sw=2 expandtab

少し幸せになれました。普段は 4tab なんです。

cond もどき

| 22:49 |  cond もどき - Going My Ruby Way を含むブックマーク はてなブックマーク -  cond もどき - Going My Ruby Way  cond もどき - Going My Ruby Way のブックマークコメント

LISP の cond みたいなことをしたかったので、ちょっと書いてみました。

実行するか否か判定する proc と実際の処理の proc を組にして持つ。

(下の例では is_even が真の場合 mul10 を実行)

proc は 1引数。条件に合う1組のみ実行。後の詳しいことはスクリプト参照してください。

cond.rb

#!/usr/bin/env ruby
# -*- coding: utf-8 -*-

require 'object'

class Cond
  def initialize(selectors=nil, &block)
    @selectors = selectors

    cascade(&block)
  end

  attr_reader :selectors

  def add(selectors={})
    @selectors.nil?           ? @selectors = selectors      :
    @selectors.kind_of?(Hash) ? @selectors.merge(selectors) :
                                @selectors.unshift(*selectors)
    self
  end

  alias << add

  def to_proc
    lambda do |a|
      f = @selectors.find {|s,| s[a]}
      f ? f.last.call(a) : a
    end
  end
end

if __FILE__ == $0
  require 'test/unit'

  class TestCond < Test::Unit::TestCase
    def setup
      is_even = lambda {|n| n.even? }
      is_odd  = lambda {|n| n.odd?  }
      mul10   = lambda {|n| n * 10  }
      sub5    = lambda {|n| n - 5   }

      @cond = Cond.new( is_even => mul10, is_odd => sub5 )
     
    # @cond = Cond.new([[is_even, mul10], [is_odd, sub5]]) でも同じ
    end
    def test_cond
      assert_equal expected = [-4, 20, -2, 40, 0, 60, 2, 80, 4, 100],
                   actual   = (1..10).map(&@cond)

    end
  end
end

# vi:set ts=2 sw=2 et fenc=UTF-8:

...なんかイマイチ。

----

FizzBuzz をやってみました。(irb -r cond)

>> c = Cond.new(
?> -> n { n % 15 == 0 } => -> x { 'FizzBuzz' },
?> -> n { n %  3 == 0 } => -> x { 'Fizz' },
?> -> n { n %  5 == 0 } => -> x { 'Buzz' })
=> #<Cond:0x00000001f141f8 @selectors= ....
......
>> (1..100).map(&c).each {|x| p x }
1
2
"Fizz"
4
"Buzz"
"Fizz"
7
8
"Fizz"
"Buzz"
11
"Fizz"
13
14
"FizzBuzz"
16
:
>>

これは、何となくはまってる気がする。

minitest/unit についてのメモ

| 20:50 | minitest/unit についてのメモ - Going My Ruby Way を含むブックマーク はてなブックマーク - minitest/unit についてのメモ - Going My Ruby Way minitest/unit についてのメモ - Going My Ruby Way のブックマークコメント

テスティングフレームワークについてのメモです。

(2011/07/24 Updates by ユニットテスト - Going My Ruby Way - Rubyist)

[参考] minitest/unit ドキュメント

----

例として、前に私が書いた string.rb をテストします。

string.rb の内容(抜粋)

class String
  def remove(s)
    sub(s, '')
  end

  alias - remove

  def wrap(w)
    w1 = w.at(0) || ''
    w2 = w.at(1) || ''
    w2 = w1 if w2.empty?

    w1 + self + w2
  end

  alias ** wrap

  def at(n)
    self[n, 1]
  end
      :
   (以下、略)

テストスクリプトは以下のような感じになります。

test_string.rb

#!/usr/bin/env ruby
# -*- coding: UTF-8 -*-
require 'string'      # テスト対象を require
require 'test/unit'   # 'test/unit' を require

class TestString < Test::Unit::TestCase  # 基底クラスは Test::Unit::TestCase
  # 前処理メソッド setup
  def setup
    @str = "foobarbaz"
  end

  # 後処理メソッド teardown
  def teardown
    # 今回は何もしない
  end

  def test_wrap   # テストメソッドの名前は test_ で始める
    # 
    # どのようにあるべきか、を表明するアサーションメソッド。
    # assert_equal はその 1 つ。actual が expect と値が等しいことを
    # 表明する。値が等しい場合はテストは成功(success)、等しくない場合は
    # 不成功(failure)、テスト自体が失敗した場合は error、となる。
    assert_equal expected = "(foobarbaz)",
                 actual   = @str ** "()" 
  end

  def test_remove
    assert_equal expected = "fooz",
                 actual   = @str - "baz"
    # このアサーションは不成功(failure)になる。
    # この場合、テスト対象のバグでなくて、期待値(expected) の
    # 記述ミス。正しくは expected = "foobaz"。
  end

  def test_ouch
    ouch!   
    # (露骨な例ですが)これは文法違反(NoMethodError)なので
    # テスト自体の失敗(error)になる。
  end
end

# vim:set ts=2 sw=2 et fenc=UTF-8:

テストメソッドは

  setup -> test_wrap -> teardown
  setup -> test_remove -> teardown 

のように毎回前処理/後処理の setup/teardown に挟まれて呼び出されます。

では、実行します。するとレポートが表示されます。

$ ruby test_string.rb
Loaded suite test_string
Started
EF.
Finished in 0.001034 seconds.

  1) Error:
test_ouch(TestString):
NoMethodError: undefined method `ouch!' for #<TestString:0x00000000b5d178>
    test_string.rb:37:in `test_ouch'

  2) Failure:
test_remove(TestString) [test_string.rb:28]:
<"fooz"> expected but was
<"foobar">.

3 tests, 2 assertions, 1 failures, 1 errors, 0 skips

Test run options: --seed 47747

3 つのテストのうち、1件不成功(failure)、1件エラー(error) です。

error については(たいていの場合)テストスクリプトの修正で対処します。

  test_ouch のメソッド定義を削除。(いらないものなので)

failure については(たいていの場合)テスト対象をデバッグして対処します。

しかし、上の例のようにテスト仕様が間違っている場合もあります。

今回は、テストスクリプトの test_remove を以下のように修正します。

  def test_remove
    assert_equal expected = "foobaz",    # 期待値を正しいものに修正
                 actual   = @str - "bar"
  end

再度、実行します。

$ ruby test_string.rb 
Loaded suite test_string
Started
..
Finished in 0.000594 seconds.

2 tests, 2 assertions, 0 failures, 0 errors, 0 skips

Test run options: --seed 13629

今度は 0 failures, 0 errors 。テスト成功です。

----

Ruby1.9 では minitest が使用できます。

require 'minitest/unit'
require 'minitest/autorun'

class TestFoo < MiniTest::Unit::TestCase
  def setup
     ....
  end

  def teardown
     ....
  end

  def test_....
     ...
  end

'test/unit'を require すると minitest の互換モードが走るらしいです。(test/unit と見た目変わらないようです)

------

その他のアサーション。いろいろありますが数例だけ挙げます。

   assert_raises(TypeError) do
     ....   # 例外を起こすはずのコード
   end

   assert_not_raised do
     ....   # 例外を起こさないはずのコード
   end

   assert_block do
     ....   # ブロックを評価して真ならテストパス。汎用。
   end

------

tips。assert_equal の 3番目の引数はメッセージ。

最初のテスト不成功になるスクリプトの test_remove を以下のように書くと、

  def test_remove
    assert_equal expected = "fooz",
                 actual   = @str - "bar",
                 message  = "文字列から文字列を削除するテスト"
  end

実行した時のレポートに不成功した時のメッセージが表示されます。

$ ruby test_string.rb
Loaded suite test_string
Started
F.
Finished in 0.000925 seconds.

  1) Failure:
test_remove(TestString) [test_string.rb:28]:
文字列から文字列を削除するテスト.
<"fooz"> expected but was
<"foobar">.

2 tests, 2 assertions, 1 failures, 0 errors, 0 skips

Test run options: --seed 52601

その他のアサーションメソッドにもメッセージが指定できます。詳しくは以下を参照。

余談。assert_equal の引数の指定に expected = とかを書いているのは分かりやすくするためです。もちろん無くていいです。

メッセージ指定の時に、カンマを忘れて下のように書くと

  def test_remove
    assert_equal expected = "fooz",
                 actual   = @str - "bar"    # <-- カンマ(,)を忘れる
                 message  = "文字列から文字列を削除するテスト"
  end

以下のように解釈されてしまいます。

  def test_remove
    assert_equal(expected = "fooz", actual = @str - "bar") 
    message  = "文字列から文字列を削除するテスト"
  end

よって message が assert_equal のオプションと見做されなくなってしまいます。

注意しましょう。(私、何回もやりました。。。)

----

tips(かな?)。こんな風にも書けます。

(t メソッドを作らず、proc (Ruby1.9 なら -> も可) でもいいです)

  def t(&block)
    Proc.new(&block)
  end

  def test_remove
    {
      t{ @str - ""    }      => t{ "foobarbaz"  },
      t{ @str - "foo" }      => t{ "barbaz"     },
      t{ @str - "bar" }      => t{ "foobaz"     },
      t{ @str - "baz" }      => t{ "foobar"     },
      t{ @str - "fo"  }      => t{ "obarbaz"    },
      t{ @str - "ba"  }      => t{ "foorbaz"    },

      t{ @str - "xyz" }      => t{ "foobarbaz"  },
      t{ @str - "fa"  }      => t{ "foobarbaz"  },

      t{ @str - /ba./ }      => t{ "foobaz"     },
      t{ @str - /ba.$/}      => t{ "foobar"     },

    }.each do |actual, expected|
      assert_equal expected[], actual[]
    end
  end

このテストの 10 個のアサーションは全部成功します。

----

tips。テストスクリプト実行時に --name オプションでテストするテストメソッドを個別に指定できます。以下は、--name で test_wrap を指定した例です。

$ ruby test_string.rb --name test_wrap
Loaded suite test_string
Started
.
Finished in 0.000580 seconds.

1 tests, 1 assertions, 0 failures, 0 errors, 0 skips

Test run options: --seed 24092 --name "test_wrap"

------

tips。サブディレクトリ test の test_*.rb をテストする Rakefile の例です。

(『Ruby ベストプラクティス』にも載ってます)

#!/usr/bin/env ruby
# -*- coding: UTF-8 -*-
require 'rake/testtask'

task :default => [:test]

Rake::TestTask.new do |test|
  test.libs << "test"
  test.test_files = Dir["test/test_*.rb"]
  test.verbose = true
end

# vi:se ts=2 sw=2 et fenc=UTF-8:

rake の実行は、以下のようにします。

$ rake test
または
$ rake

-------

tips。モジュールのメソッドをテストするとき、以下のようにします。

  module Foo
    # テスト対象のモジュール
    def a
        :
  end

  class TestFooModule
    include Foo  # ダミーのクラスに include してテストする
  end

  class TestFoo < Test::Unit::TestCase
    def setup
      @c = TestFooModule.new   # インスタンスを作る
    end

Foo を include したクラスのインスタンスを直接作る例。

  module Foo
    # テスト対象のモジュール
    def a
        :
  end

  class TestFoo < Test::Unit::TestCase
    def setup
      @c = Class.new{ include Foo }.new   # インスタンスを直接作る
    end
        : 

-------

有用そうなライブラリ。『Ruby ベストプラクティス』では

  • モックには flexmock ライブラリ
  • 偽データ生成には faker ライブラリ

を勧めています。

-------

参考書籍

第1章で TDD / テスティングフレームワークについて書かれている。

test/unit の本。

Rubyを256倍使うための本 極道編

Rubyを256倍使うための本 極道編

知らなかった Enumerable のメソッド (その2)

| 13:46 |  知らなかった Enumerable のメソッド (その2) - Going My Ruby Way を含むブックマーク はてなブックマーク -  知らなかった Enumerable のメソッド (その2) - Going My Ruby Way  知らなかった Enumerable のメソッド (その2) - Going My Ruby Way のブックマークコメント

1.9.2 のドキュメント見てたら、知らなかった Enumerable のメソッドがありました。

メモその2。

----

each_cons - 重複ありで n 要素ずつに区切りブロックに渡す

>> (1..10).each_cons(3){|v| p v }
[1, 2, 3]
[2, 3, 4]
[3, 4, 5]
[4, 5, 6]
[5, 6, 7]
[6, 7, 8]
[7, 8, 9]
[8, 9, 10]
=> nil

----

each_slice - n 要素ずつに区切りブロックに渡す

>> (1..10).each_slice(3) {|a| p a}
[1, 2, 3]
[4, 5, 6]
[7, 8, 9]
[10]
=> nil

----

each_entry ... よくわからん。each との違いは以下。

class C
  include Enumerable
  def each
    yield(1,2)
  end
end

c = C.new

c.each_entry {|n| p n }  # => [1,2]
c.each       {|n| p n }  # => 1

c.each_entry {|n,| p n}  # => 1
c.each       {|n,| p n}  # => 1

c.each_entry {|_,n| p n} # => 2
c.each       {|_,n| p n} # => 2

c.each_entry {|*a| p a}  # => [[1,2]]
c.each       {|*a| p a}  # => [1,2]

ブロックが 1引数でも yeild の引数を配列で渡す、ってこと?

----

find_index - 条件に合う添字を見つける

(1..10).find_index  {|i| i % 5 == 0 and i % 7 == 0 }   #=> nil
(1..100).find_index {|i| i % 5 == 0 and i % 7 == 0 }   #=> 34

----

first - 最初の n要素の配列。結果が 1 要素の場合、値をそのまま返す。

>> [1,2,3].first
=> 1
>> [1,2,3].first(2)
=> [1, 2]

----

group_by - ブロックの評価結果をキー、対応要素の配列を値としたハッシュを返す

>> (1..6).group_by {|i| i%3}
=> {1=>[1, 4], 2=>[2, 5], 0=>[3, 6]}

----

none? - (ブロックの評価値 or 要素) がすべて偽なら真を返す

>> [].none?
=> true
>> [nil].none?
=> true
>> [nil,false].none?
=> true
>> [1,2,3].none? {|n| n > 10}
=> true
>> [true,false].none?
=> false

----

one? - (ブロックの評価値 or 要素) がひとつだけ真なら真を返す

>> [].one?
=> false
>> [nil].one?
=> false
>> [nil,false].one?
=> false
>> [1,2,3].one? {|n| n > 10}
=> false
>> [1,2,3].one? {|n| n > 2}
=> true

----

partition - ブロックの条件を満たす要素とそうでない要素の配列に分ける

>> [10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0].partition {|i| i % 3 == 0 }
=> [[9, 6, 3, 0], [10, 8, 7, 5, 4, 2, 1]]

----

slice_before - (ブロックの条件 or 引数のパターン)を満たす要素の前で分割する

>> e = [1,2,3,4,5].slice_before {|n| n.even? }
=> #<Enumerator: #<Enumerator::Generator:0x00000000807cf8>:each>
>> e.each {|x| p x}
[1]
[2, 3]
[4, 5]
=> nil

>> e = ["a", "b", "-", "c", "d", "-", "e"].slice_before(/^-/)
=> #<Enumerator: #<Enumerator::Generator:0x00000000819fe8>:each>
>> e.each {|x| p x}
["a", "b"]
["-", "c", "d"]
["-", "e"]
=> nil

知らなかった Enumerable のメソッド

| 10:38 |  知らなかった Enumerable のメソッド - Going My Ruby Way を含むブックマーク はてなブックマーク -  知らなかった Enumerable のメソッド - Going My Ruby Way  知らなかった Enumerable のメソッド - Going My Ruby Way のブックマークコメント

1.9.2 のドキュメント見てたら、知らなかった Enumerable のメソッドがありました。

メモメモ。

----

cycle - 要素を無限に繰り替えす

>> [1,2,3,4].zip([:px].cycle).map {|n,unit| "#{n}#{unit}"}
=> ["1px", "2px", "3px", "4px"]

>> [1,2,3].cycle {|n| p n}
1
2
3
1
2
3
:   # 無限に...

chunk - チャンクを要素とする enumrator を返す。(難しい。。。)

>> e = [1,2,3].chunk {|n| "#{n}px" }
=> #<Enumerator: #<Enumerator::Generator:0x00000000c05850>:each>
>> e.each {|x| p x }
["1px", [1]]
["2px", [2]]
["3px", [3]]
=> nil

>> e = [1,2,3].chunk {|n| p n }    # p n するブロックを与える
=> #<Enumerator: #<Enumerator::Generator:0x0000000082a050>:each>
>> e.each {|n| n }                 # この時点でブロック実行 
1
2
3
=> nil

>> base = 100
=> 100
>> e = [1,2,3].chunk {|n| n * base }
=> #<Enumerator: #<Enumerator::Generator:0x00000000bd9818>:each>
>> e.each {|n| p n }
[100, [1]]
[200, [2]]
[300, [3]]
=> nil
>> base = 200
=> 200
>> e.each {|n| p n }   # クロージャー
[200, [1]]
[400, [2]]
[600, [3]]
=> nil

>> e = [1,2,3].chunk(state=[]) {|n, memo| n * base }
...
# memo は state を dup したもの。
# (dup できない Numeric などは state で渡せない)

とりあえず、チャンクは res = ブロックの評価結果 と data = 元のデータの、

  [res, [data]]

Array という解釈でいいのだろうか。

で、ブロックの評価は enumerator の each(など) が呼ばれた時点で行なわれる(遅延評価)。

----

flat_map

>> [[1,2],[3,4]].flatten(1).map {|i| i }   #=> [1, 2, 3, 4]
=> [1, 2, 3, 4]

この例では、

[[1,2],[3,4]].flatten.map {|i| i }

と同じ。flat_map の存在意義は?

----

count

引数やブロックを指定できたらしい。

>> a = [1, 2, 4, 2]
=> [1, 2, 4, 2]
>> a.count
=> 4
>> a.count(2)
=> 2
>> a.count{|x| x.even?}
=> 3

----

drop - 最初の n 要素を捨て、残りを配列として返す

>> [1,2,3,4].drop(1)       # cdr な感じ
=> [2, 3, 4]

drop_while


>> [1,2,3,4].drop_while {|n| n < 3}
=> [3, 4]
>> [1,2,3,4].drop_while {|n| n > 3}
=> [1, 2, 3, 4]

>> e = [1,2,3,4].drop_while
=> #<Enumerator: [1, 2, 3, 4]:drop_while>
>> e.each {|n| n < 3}
=> [3, 4]

----

take - 最初の n 要素を配列として返す

>> [1,2,3,4].take(2)
=> [1, 2]
----

take_while

>> [1,2,3,4].take_while {|n| n < 3}
=> [1, 2]
>> [1,2,3,4].take_while {|n| n > 3}
=> []
>> e = [1,2,3,4].take_while
=> #<Enumerator: [1, 2, 3, 4]:take_while>
>> e.each {|n| n < 3}
=> [1, 2]

まだありますが、とりあえずここまで。

---------------------

chunk を試してる時ハマったところ。 p の返り値が 1.9 で変更されてました。

ruby1.8.7

>> p 10
10
=> nil

ruby1.9.2

>> p 10
10
=> 10