pepe_la_phew's diary

LL系の話など

ifstreamをモック化

gmockを使えば他クラス実装への依存を減らせるので有り難いのですが、今回のお題であるifstreamのように非virtual・既存・変更不可なクラスをモック化したい場合にはちょっとしたトリックが必要です。
トリックの内容は公式のクックブックの通りですが、以下の3点です:

  • モック化したいクラスを継承するのではなく、別クラスとして定義する。
    • 下記の例ではifstreamの継承ではなくMockStreamを作成。
  • テスト対象クラスをテンプレートクラスとする。テンプレートパラメータとしてモック化対象クラスを指定する。
    • 例ではBinaryReaderクラスをテンプレート化。テンプレートパラメータとして、プロダクトコードではifstreamを、テストコードではMockStreamを指定。
  • モック化対象クラスを参照ではなくポインタで保持する。

以下のサンプルは、ifstreamを使ってバイナリデータを読むためのクラスと、そのテストコードです。

#include <gtest/gtest.h>
#include <gmock/gmock.h>
using namespace ::testing;

class MockStream {
public:
    MOCK_METHOD2(read, MockStream&(char* s, int n));
};

template<class T>
class BinaryReader {
    T* const m_stream;
public:
    BinaryReader(T& stream)
        : m_stream(&stream)
    {}

    template<typename U> void read(U& data) {
        m_stream->read(reinterpret_cast<char*>(&data), sizeof(data));
    }
};
TEST(BinaryReader, read_char) {
    StrictMock<MockStream> stream;

    EXPECT_CALL(stream, read(A<char*>(), TypedEq<int>(1)))
        .Times(1)
        .WillOnce(DoAll(SetArgPointee<0>(0x01),
                        ReturnRef(stream)));

    BinaryReader<MockStream> reader(stream);
    char data;
    reader.read(data);

    ASSERT_EQ(char(0x01), data);
}

モック対象クラスをT*とポインタで保持してある点について補足を。
ポインタは気持ち悪いので参照で保持しておきたいところです。こんな感じですね。

template<class T>
class BinaryReader {
    T& m_stream;
public:
    BinaryReader(T& stream)
        : m_stream(stream)
    {}
};

これをコンパイルするにはモッククラスにコピーコンストラクタを定義する必要があります。
しかしEXPECT_CALLが見ているのはstreamであってコピー先のインスタンス(BinaryReader::m_stream)とは別物なので、モックがうまく動きません。
結果としてポインタで持たざるを得ないということになります。

…この結論に辿り着くまで随分時間がかかってしまったので、以下のようにモックを使わず直接ファイルを読むテストを書く方が早いと思ってしまいました :p

TEST(BinaryReader, read_char) {
    std::ifstream stream("test.dat");
    BinaryReader reader(stream);
    char data;
    reader.read(data);

    ASSERT_EQ(char(0x41), data);
}

誰かの時間節約に役立ちますように(未来の自分を含む)。

PHPUnitサンプル

PHPユニットテストを書くのはこんなに簡単、というサンプル。
題材は黄表紙『達人プログラマー』より。

■テストコード:largestTest.php

<?php
require_once("largest.php");

class largestTest extends PHPUnit_Framework_TestCase
{
    public function testOne() {
        $this->assertEquals(1, largest(array(1)));
    }

    public function testDups() {
        $this->assertEquals(9, largest(array(9, 7, 8, 9)));
    }

    public function testOrder() {
        $this->assertEquals(9, largest(array(9, 8, 7)));
        $this->assertEquals(9, largest(array(7, 9, 8)));
        $this->assertEquals(9, largest(array(7, 8, 9)));
    }

    public function testNegative() {
        $this->assertEquals(-7, largest(array(-9, -8, -7)));
    }

    public function testEmpty() {
        try {
            largest(array());
            die("ここへは来ない");
        }
        catch (Exception $e) {
            $this->assertTrue(TRUE);
        }
    }
}
?>

■プロダクトコード:largest.php

<?php
define('PHP_INT_MIN', ~PHP_INT_MAX);

function largest($list) {
    if (count($list) == 0) {
        throw new Exception("空のリストが渡された");
    }

    $max = PHP_INT_MIN;
    foreach ($list as $i) {
        if ($max < $i) {
            $max = $i;
        }
    }
    return $max;
}
?>


この本、内容が古くなっているだけに絶版なのかな。ユニットテストの章は現役だと思うが…

達人プログラマー―ソフトウェア開発に不可欠な基礎知識 バージョン管理/ユニットテスト/自動化 (Ascii software engineering series)

達人プログラマー―ソフトウェア開発に不可欠な基礎知識 バージョン管理/ユニットテスト/自動化 (Ascii software engineering series)

  • 作者: デビッドトーマス,マイククラーク,アンドリューハント,David Thomas,Mike Clark,Andrew Hunt,長瀬嘉秀,テクノロジックアート
  • 出版社/メーカー: アスキー
  • 発売日: 2005/03
  • メディア: 単行本
  • 購入: 3人 クリック: 91回
  • この商品を含むブログ (70件) を見る

JSONスキーマで妥当性確認

JSONスキーマでJSONファイルをvalidateするだけのコード。

#!/usr/bin/env ruby

require 'rubygems'
require 'json-schema'

exit 1 unless ARGV.length == 2

schema = ARGV[0]
json = ARGV[1]

errors = JSON::Validator.fully_validate(schema, json)

errors.each do |error|
  puts error
end

スキーマの書き方に自信をもてないので、上記スクリプトに .../gems/json-schema-1.0.9/resources/draft-03.json (JSON Schema自体のスキーマ定義)とスキーマファイルを指定してvalidateしながら書いている。

【情報まとめ】WebAPIの仕様化やテストについて

テスト記述

Webを支える技術 -HTTP、URI、HTML、そしてREST (WEB+DB PRESS plus)

Webを支える技術 -HTTP、URI、HTML、そしてREST (WEB+DB PRESS plus)


サービスデザインパターン SOAP/WSDLとRESTful Webサービスの基本的な設計ソリューション

サービスデザインパターン SOAP/WSDLとRESTful Webサービスの基本的な設計ソリューション


モダンPerl入門 (CodeZine BOOKS)

モダンPerl入門 (CodeZine BOOKS)


Cucumber Recipes: Automate Anything With Bdd Tools and Techniques

Cucumber Recipes: Automate Anything With Bdd Tools and Techniques

Nokogiri+CucumberにてXMLの妥当性確認

XMLに対応したXSDを用意し、それをNokogiriで読んでvalidateする。
その結果をCucumberで受ける。
…というだけのものです。

■features/validate_xml.feature

機能: XSDによりXMLの妥当性を確認する

シナリオ: XMLスキーマを読んで妥当性を確認

    前提: XMLドキュメント "sample1.xml" を読む
    かつ: スキーマ "sample1.xsd" を読む
    もし: ドキュメントがスキーマに準拠している
    ならば: ドキュメントは妥当性確認された

■features/step_definitions/xml_validation.rb

前提 /^: XMLドキュメント "(.*?)" を読む$/ do |fname|
  @doc = Nokogiri::XML(File.read(fname))
end

前提 /^: スキーマ "(.*?)" を読む$/ do |fname|
  @xsd = Nokogiri::XML::Schema(File.read(fname))
end

もし /^: ドキュメントがスキーマに準拠している$/ do
  @valid = true
  @xsd.validate(@doc).each do |error|
    @valid = false
    puts "XML not schema valid ([#{error.line}-#{error.column}] #{error.message})"
  end
end

ならば /^: ドキュメントは妥当性確認された$/ do
  @valid.should be_true
end

■features/support/env.rb

require 'nokogiri'

PlantUMLでアクティビティ図を描く

"IT専科TOP > UML入門 > アクティビティ図(Activity Diagram)"の最後にある例をPlantUMLで作ってみた。
試行錯誤したものの完全には再現できなかった。

f:id:pepe_la_phew:20120911211537p:plain

「サーバー」を先に定義することでHTTPとFTPをサーバーのパーティションに属させつつ、クライアント側にifを入れることで分岐をクライアントのパーティションに属させることはできた。
パーティションを綺麗に並べる方法はわからない。
流れを横方向にする方法もわからない。
(-->を->や-right->に変えてみたが、良い結果ではなかった。Graphvizならrankdir=LRなのだが)

ソース:

  partition サーバー {
    HTTP --> (*)
    FTP --> (*)
  }

  partition クライアント {
    (*) --> 選択
    選択 if "" then
      --> HTTP
    else
      --> FTP
    endif
  }

XMLから指定ノードを削除するパッチ

XMLファイルにノード削除パッチをあてたいので、Ruby + Nokogiriで書いてみた。

# -*- coding: utf-8 -*-
require 'rspec'
require 'nokogiri'

class XmlPatch
  def initialize(xml)
    @doc = Nokogiri.XML(xml)
  end

  def del_node(tag_name, attr_name, attr_val)
    counter = 0

    @doc.root.xpath("//#{tag_name}").each { |node|
      if node.has_attribute?(attr_name) and node.attr(attr_name) == attr_val then
        node.remove
        counter += 1
      end
    }

    counter
  end

  def to_s
    @doc.to_s
  end
end

describe 'XmlPatchクラス' do
  describe 'コンストラクタ' do
    it 'xml属性を設定する' do
      patch = XmlPatch.new('<A><B><C tag="TAG"/></B></A>')

      patch.to_s.should == "<?xml version=\"1.0\"?>\n<A>\n  <B>\n    <C tag=\"TAG\"/>\n  </B>\n</A>\n"
    end
  end

  describe 'del_node_by_id' do
    it '指定タグのうち指定idを持つノードを削除し、削除した件数を返却する' do
      patch = XmlPatch.new('<A><B><C id="01234"/><C id="56789"/></B><C id="01234"/></A>')
      patch.del_node('C', 'id', '01234').should == 2
      patch.to_s.should == "<?xml version=\"1.0\"?>\n<A>\n  <B>\n    <C id=\"56789\"/>\n  </B>\n</A>\n"
    end
  end
end