Hatena::Groupstudyroom

文::字

2010-07-18

[] 単体テスト結合テスト21:16  単体テスト? 結合テスト? - 文::字 を含むブックマーク

Mock とか Stub を駆使してテストを書いていると、現実の世界とはあまりにかけ離れた世界なので不安になる。

Mock や Stub がどれだけ擬装対象のクラスに近いかを評価する指標みたいのが必要なのか?

だから、よく使われる MockObject をライブラリとして提供するのにもそれなりに意味があるんだろうと思う。HTTP リクエストのモックを作ったりするのすごくめんどくさそうだし。

あと 1 つのコード修正に対する影響範囲を検出するという意味では、単体テストだけじゃなくて、結合テストも別途必要っぽい。

性能テスト、結合テストシステムテストとか。いろいろ用意するべきなんだろうけど、テストばっかり作ってたらきりがないなぁ…。

[] コンストラクタの中で別のクラスのインスタンスを生成するやつ 20:55  コンストラクタの中で別のクラスのインスタンスを生成するやつ - 文::字 を含むブックマーク

//...
var Markov = function() {
    this.chain = {}
    this.segmenter = new TinySegmenter()
}
//...
var markov = new Markov()
//...

他人が作った便利クラスを使うときとかやりがちだけど、テストしづらくなるので基本的にはあんまり良くないんだろうなと最近よく思う。

オブジェクト指向言語常套句としては、別途インタフェースを用意してそのインタフェースを実装したクラスのオブジェクトを渡してやるのが良いような。Java のコードとかに多そう。

JavaScriptRuby で、かつ便利クラスの機能をちょっとだけ使いたいような場合には、匿名関数を渡してやるとシンプルにまとまる気がする。

2010-07-14

[][] PHPUnit でのテストの命名規則 23:05  PHPUnit でのテストの命名規則 - 文::字 を含むブックマーク

ファイル名を HogeTest.php のように Test.php で終わらせないとテストケースとして認識してくれない。ディレクトリを指定してテストを一括で実行すると分かる。

たとえば以下のような構成のとき、phpunit test としても Naha.php の中のテストは実行されない。

test ディレクトリ --- HogeTest.php
                  |-- FugaTest.php
                  |-- Naha.php

2010-07-13

[][] クラス自体をスタブにする 23:13  クラス自体をスタブにする - 文::字 を含むブックマーク

PHPUnit の TestCase#getMock の第 4 引数を利用できないかなと思ったけど、やっぱりダメ。

<?php

class SomeClass {
    private $obj;
    public function __construct() {
        $this->obj =  new OtherClass();
    }   
    public function doSomething() {
        $this->obj->doSomething();
    }   
}

class SomeClassTest extends PHPUnit_Framework_TestCase {
    private $obj;
    public function setUp() {
        $mockClass = $this->getMock('MockOtherClass', array('doSomething'), array(), 'OtherClass');
        $mockClass->expects($this->any())
                  ->method('doSomething')
                  ->will($this->returnValue('hello from MockOtherClass#doSomething'));
        $this->obj = new SomeClass();
    }   
    public function testDoSomething() {
        $ret = $this->obj->doSomething();
        $this->assertEquals($ret, 'hello from MockOtherClass#doSomething');
    }   
}

[][] RSpec 挙動確認 23:36  RSpec 挙動確認 - 文::字 を含むブックマーク

describe 'Array' do
  it {
    Array.stub!(:size).and_return(1)
    [].size.should == 1 # fail
  }
  it {
    array = []
    array.stub!(:size).and_return(1)
    array.size.should == 1 # success
  }
end

前者は Array のクラスメソッド size についてのスタブだから、1 は返ってこない、期待通りの挙動。

ただ、「Array がインスタンス化されて、さらに size というメッセージを受け取ったら、1 を返す」みたいなスタブを作れると便利な気もする。

2010-07-12

[] 機能追加時に既存のコードを修正しないことの価値 00:58  機能追加時に既存のコードを修正しないことの価値 - 文::字 を含むブックマーク

要件が増えたとき対象となるメソッドにその分のコードを追加していくとメソッドが長くなってモンスターっぽくなるし、単体テストが書きづらくなる。if-else が増えてコードが読みづらくもなる。

上記のように「その場に追加する」以外の方法として「レガシーコード改善ガイド」に紹介されているような「スプラウト・メソッド」「スプラウト・クラス」「ラップ・メソッド」「ラップ・クラス」といった手法を使う。

[][] クラス自体をスタブにする 00:31  クラス自体をスタブにする - 文::字 を含むブックマーク

RSpec で Array.stub!(:size).and_return(5) って書くみたいに簡単にクラス自体をスタブ化したいんだけど、調べた限りではやり方が見つからなかった。どうやるのがいいんだろう?

eval して動的にクラス定義する方法を考えてみたけど、これだと test メソッドが 2 つ以上になったときに StubClass() 関数が 2 回以上実行されて「クラスは再定義できません」という旨のエラーになる。

runkit とか使えばいいのかな…。

<?php

/***********************************************/
/* テスト対象のクラス                          */
/***********************************************/

class SomeClass {
    private $obj;
    public function __construct() {
        $this->obj = new OtherClass(); 
    }
    public function doSomething() {
        return $this->obj->doSomething();
    }
}

/***********************************************/
/* ヘルパー関数                                */
/***********************************************/

function StubClass($className, $arrMethods=null) {
    $methods = '';
    if($arrMethods && count($arrMethods) > 0) {
        foreach($arrMethods as $name => $value) {
            $value = serialize($value);
            $methods .= "public function $name() { return unserialize('$value'); }";
        }
    }
    eval("class $className {" . $methods . "}");
    return $className;
}

/***********************************************/
/* テスト本体                                  */
/***********************************************/

class SomeClassTest extends PHPUnit_Framework_TestCase {
    private $obj;
    public function setup() {
        StubClass('OtherClass', array('doSomething' => 'something done.'));
        $this->obj = new SomeClass();
    }
    public function testDoSomething() {
        $this->assertEquals($this->obj->doSomething(), 'something done.');
    }
}

2010-07-11

[][] PHPUnit でモックオブジェクトを作るには対象クラスが予め定義されていることが必要 (?) 14:02  PHPUnit でモックオブジェクトを作るには対象クラスが予め定義されていることが必要 (?) - 文::字 を含むブックマーク

必要だったような気がするんだけど、以下の class Fuga{} をコメントアウトしても通る。

<?php
class Hoge {
    public function hogeMethod($fuga) {
        return $fuga->fugaMethod();
    }
}
//class Fuga {} // Fuga が予め定義されていることが必要?

class HogeTest extends PHPUnit_Framework_TestCase {
    public function setUp() {
        $this->hoge = new Hoge();
    }
    public function testHogeMethod() {
        $this->fuga = $this->getMock('Fuga', array('fugaMethod'));
        $this->fuga->expects($this->once())
                   ->method('fugaMethod')
                   ->will($this->returnValue('Fuga#fugaMethod called'));

        $ret = $this->hoge->hogeMethod($this->fuga);

        $this->assertEquals($ret, 'Fuga#fugaMethod called');
    }
}

[] 疑問点 13:44  疑問点 - 文::字 を含むブックマーク

  • プライベートメソッドのテストをテストスイートに組み込むべきか?
    • 外部からアクセスされないということは必ずしもテストする必要はないけど、複雑なロジックが書かれている場合にはテストしたほうがいいような気がする

[][] PHPUnit で同じクラスの別のメソッドをモック化する 13:34  PHPUnit で同じクラスの別のメソッドをモック化する - 文::字 を含むブックマーク

TestCase#getMock の第 2 引数にモック化してほしいメソッドを指定すると、そのメソッドだけモック化して、あとは本物のメソッドとしてインスタンス化してくれる。

<?php
class SomeClass {
    public function methodA() {
        $me = 'from method A';
        return $me . ' (' . $this->methodB() . ')';
    }
    public function methodB() {
        // not implemented yet
    }
}

class SomeClassTest extends PHPUnit_Framework_TestCase {
    private $obj;
    public function setUp() {
        $this->obj = $this->getMock('SomeClass', array('methodB'));
    }
    public function testMethodA() {
        $this->obj->expects($this->once())
                  ->method ('methodB')
                  ->will   ($this->returnValue('from mock method B'));

        $ret = $this->obj->methodA();

        $this->assertEquals($ret, 'from method A (from mock method B)');
    }
}

[] 良い単体テストの指標 12:32  良い単体テストの指標 - 文::字 を含むブックマーク

優先度の高い順に。思いついたらあとで付け足す。

  • コードカバレッジが高いこと
    • テスト対象のコードに含まれる分岐を網羅していること
    • よっぽどのことが無い限り 100 % を目指す
    • なるべく少ないテストコードでなるべく高いコードカバレッジを目指す
  • 外部に依存していないこと
  • テストが読みやすいこと
    • BDD 系のテストフレームワークはマストじゃないにしても、意図を明確にしてコードを書く
    • テストデータをハードコードすることでごちゃごちゃしているようにみえてしまうのはある程度仕方ないのかな…
      • どこまでデータが大きくなったらフィクスチャとして切り出すべきか?
  • 冗長ではないこと
    • 他クラスのテストでも使い回せるコードはどんどんヘルパーとして切り出す
    • テストハーネスの上にもう 1 個ヘルパーによる階層があるイメージ

2010-07-08

[][]既に定義されている関数をモック化する 08:07 既に定義されている関数をモック化する - 文::字 を含むブックマーク

PHP は言語に組み込まれている基本機能のインタフェースが全然オブジェクト指向じゃないので、定義済みの関数をモック化してテストできるようにしたい。

調べてみたら PHPMockFunction というライブラリを使うとできるっぽい。使うには PECL の runkit という拡張モジュールを先にインストールしておく必要がある。詳しいインストール方法はこのへんに書いてある。

で file_get_contents とかをモックにしようとしたんだけど、「組み込み関数は再定義できません」的なエラーが出た。どうやら php.ini で runkit.internal_override を有効にする必要があるらしい。

で、上記の値を有効にしたら単体では動くようになったんだけど、PHPUnit と組み合わせて使うと Segmentation Fault が出てしまう。

以下のソースを runkit-0.9.tgz に上書きして ./configure ; make ; make install し直したらちゃんと動いた。

(make test すると半分ぐらい失敗してるけどまぁいいや。)

2010-07-07

[][] 外部からアクセスできない変数をモックにする 21:59  外部からアクセスできない変数をモックにする   - 文::字 を含むブックマーク

他の人のソースをたくさん読んでいるわけではないのでよく使われる方法なのかどうかよく分からないんだけど、Object#instance_variable_get を使えばオブジェクトが持ってるプライベートな変数をばんばんモックにすることができるなと思った。

よくあるスクレイピング用のクラス。

require 'rubygems'
require 'mechanize'

class HatenaDiary

  def initialize(user)
    @user = user
    @agent = WWW::Mechanize.new
  end

  def titles
    @agent.get('http://d.hatena.ne.jp/' + @user + '/archive')
    @agent.page.search('//li[@class="archive archive-section"]/a/text()')
  end

end

こんな感じで使う。

hd = HatenaDiary.new('tily')
puts hd.titles

こういうクラスの場合、WWW::Mechanize をもっと便利に使うラッパーという意味合いが強いので、当然オブジェクトの内部で WWW::Mechanize のメソッドが適切に呼び出されているかをテストしたい。

new したあとで、instance_variable_get('@agent') とかすると普通に HatenaDiary クラス内部にある @agent をモック化できる。

require 'spec'

describe HatenaDiary do

  before do
    @hd = HatenaDiary.new('tily')
    @agent = @hd.instance_variable_get('@agent')
  end

  it '#titles return entry titles' do
    @agent.should_receive(:get).with(
      'http://d.hatena.ne.jp/tily/archive'
    ).once
    @agent.page.should_receive(:search).once.with(
      '//li[@class="archive archive-section"]/a/text()'
    ).and_return('dummy titles')
    ret = @hd.titles
    ret.should == 'dummy titles'
  end

end

2010-01-02

[][]RSpec、describe のネスト 21:13 RSpec、describe のネスト - 文::字 を含むブックマーク