こんにちは、高木です。
今回は、PHPを使ってC++のクラスをテストするコードを生成してみることにします。といっても、テストケースを自動生成するところまでやるのではなく、あくまでもテスト関数を挿入するだけにとどめます。
まず、テストの対象となるクラスを作ります。何でもいいのですが、単純なカウンタークラスを作ってみましょう。こんな感じです。
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
#include <stdexcept> class counter { public: counter(unsigned value = 0) : value_(value) { } unsigned get() const { return this->value_; } void set(unsigned value) { this->value_ = value; } void count() { auto t = this->value_ + 1; if (t == 0) throw std::overflow_error("counter"); this->value_ = t; } private: unsigned value_; }; |
このクラスをテストする関数を作成するのですが、privateメンバーであるvalue_には外部からアクセスすることができません。この例ではgetメンバー関数でvalue_の値は取得できるのですが、ここではそういう問題ではなく、privateやprotectedのメンバーに外部からアクセスできるかどうかを問題にしています。
privateやprotectedのメンバーに外部からアクセスするには、テスト関数をメンバー関数にするか、フレンド関数にする必要があります。protectedメンバーだけなら派生クラスを作る方法もありますが、privateメンバーへのアクセスと共通にしたいので派生クラスの案は捨てます。
一番素直なのはメンバー関数です。ただ、テストのためだけの実運用では不要なメンバーを追加したくないですし、テスト時以外でテスト用のメンバー関数を削除するのものちのちいろんなことが懸念されます。そこで、今回はフレンド関数を使う方法を採用することにします。
PHPで作成する関数は、テスト関数の記述開始を宣言するbegin_test関数、記述終了を宣言するend_test関数、そしてテスト関数を実行するrun_test関数の3つとします。それぞれの定義は次のようになります。
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 |
<?php function begin_test(string $class_name): void { global $test_name; ob_start(); $test_name = $class_name; echo <<<EOT friend void test_$test_name($class_name*) { std::cout << "=== begin test $test_name ===" << std::endl; EOT; } function end_test(): void { global $test_name; echo <<<EOT std::cout << "=== end test $test_name ===" << std::endl; } EOT; unset($test_name); $contents = ob_get_contents(); ob_end_clean(); global $test; if (isset($test)) { echo $contents; } } function run_test(string $class_name): void { echo "test_$class_name(($class_name*)0);" . PHP_EOL; } ?> |
本来ならテスト関数は引数はいらないのですが、形だけでも引数を用意してあげないとコンパイルエラーになってしまいます。形式的な引数をユーザーに書かせるのも不親切なので、run_test関数で空ポインターを渡すようにしています。
ob_start関数を使ってテスト関数の定義部分だけをキャプチャし、グローバル変数$testがあればキャプチャした内容を出力するようにしています。ob_start関数によるキャプチャは入れ子にすることができますので、仮に外側でob_startを使っていたとしても問題ありません。
これらのPHPで作った関数を使ってテストコードを書いてみましょう。
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 |
<?php $test = true; ?> #include <boost/core/lightweight_test.hpp> #include <stdexcept> class counter { public: counter(unsigned value = 0) : value_(value) { } unsigned get() const { return this->value_; } void set(unsigned value) { this->value_ = value; } void count() { auto t = this->value_ + 1; if (t == 0) throw std::overflow_error("counter"); this->value_ = t; } private: unsigned value_; <?php begin_test('counter'); ?> counter c(123); BOOST_TEST_EQ(c.value_, 123); BOOST_TEST_EQ(c.get(), 123); c.count(); BOOST_TEST_EQ(c.value_, 123 + 1); c.set(~0u); BOOST_TEST_THROWS(c.count(), std::overflow_error); <?php end_test(); ?> }; #include <iostream> int main() { <?= run_test('counter'); ?> boost::report_errors(); } |
今回はBoost C++ Librariesのlightweight_testを使いましたが、テストフレームワークやライブラリは何でもかまいません(使用するフレームワークやライブラリによっては、PHPで生成するコードを変更する必要があります)。
わざわざこんなことをしなくても、#ifを使ってテスト関数を生成するかどうかを切り替えることはできます。ただ、クラス定義の中にゴチャゴチャとテストコードが混在していると見通しが悪くなります。テストコードをいっしょに書いていた方がいいと考える方もいらっしゃると思いますので、どちらがよいかは人それぞれでしょうね。
PHPで前処理すれば、PHPのコードを含む元のソースコードにはテストコードが書かれていますし、前処理後のコードからはテストコードを除去することもできます(今回の例ではグローバル変数$testを定義しなければOK)。