ソフトウェアテスト
2011年07月27日
※英語でも書いてあります。 [Boost.Test] Where is boost::test_tools::tt_detail::check_impl ? — Gist
C++向けに単体テストを書くとき、以前はCppUnitというツールを使っていたのだが、色々調べているうちに「CppUnitは記述が面倒」ということを知り、別のツールを試してみることに。
そこで、C++向けの総合ライブラリ「Boost」に入っている単体テストツール「Boost.Test」を使ってみたのだが、
を元にコードを書き
以下のコマンドでコンパイルをしても
g++ -I/PATH/TO/BOOST -L/PATH/TO/BOOST/bin.v2/libs/test/build/gcc-4.3.3/release/link-static/threading-multi -lboost_unit_test_framework soulgem_test.cpp -o soulgem_test
以下のようなエラーが出てしまう。
undefined reference to `boost::test_tools::tt_detail::check_impl( ...
ちなみに、Boostのバージョンは1.46.1、GCCのバージョンは4.3.3である。
で、ぐぐっても英語含めそれらしい結果が出なくて困っていたのだが、-lboost_unit_test_frameworkに相当するバイナリ(/PATH/TO/BOOST -L/PATH/TO/BOOST/bin.v2/libs/test/build/gcc-4.3.3/release/link-static/threading-multi/libboost_unit_test_framework.a)のあるフォルダを漁っていると、そこに「test_tools.o」なるファイルを発見。これをコンパイル時にリンク対象として追加し
g++ -I/PATH/TO/BOOST -L/PATH/TO/BOOST/bin.v2/libs/test/build/gcc-4.3.3/release/link-static/threading-multi -lboost_unit_test_framework /PATH/TO/BOOST/bin.v2/libs/test/build/gcc-4.3.3/release/link-static/threading-multi/test_tools.o soulgem_test.cpp -o soulgem_test
とすると、見事コンパイルが通った。
2011年06月05日
※2011.6.18 3:12追記:自己紹介に使ったLT資料を公開しました。
TDD(Test-Driven Development、テスト駆動開発)を実習形式で学ぶ会「TDD Bootcamp」が、2週間前のTDD Bootcamp 1.5に続いて開催されたので参加した。
今回は「お題はJavaかRubyで出題、ただし参加者側で事前準備が可能なら他の言語での実施も可」となったため、お題をC++に移植し、さらに私が最近使い始めたCppUnit(C++向け単体テストツール)の準備をした。
で、事前準備万端なつもりで会場入りしたら、@sumimさんから「C++のお題コード、VisualStudioでコンパイル出来ないよ」と言われた。焦ったが、コーディング開始までの時間でよく調べてみると、一旦VisualStudioで編集したものをUnix環境に持ち込んで編集したため、改行コードがCRLFからLFに変わってたのが原因だった(*1)。
まず、主催の@shuji_w6eさんからTDDの概要について説明があり、その後各言語担当者が自己紹介。そして今回の招待講演者・@t_wadaさんからTDDについていくつかポイントの説明があり、その後実習に移った。
C++班になったのは私を含め3人(他は@hotwatermorningさん、@asari755さん)。ちなみに全体の人数は以下の通りでした。
| 言語 | 担当スタッフ | 班数 | 人数 |
|---|---|---|---|
| Java | @irasallyさん | 5班 | 19人 |
| Ruby | @niku_nameさん | 2班 | 6人 |
| Python | @nakayoshixさん @giginetさん | 2班 | 7人 |
| Smalltalk | @sumimさん | 1班 | 6人 |
| C++ | @h_hiro_ | 1班 | 3人 |
お題として出された事例は、「ファイルにデータを一定のフォーマットで書き出す/読み込む」というコードであった。(※コードはこちら:maraigue / tddbc-sap02-legacy-cpp – Bitbucket)このコードにはテストコードがないため、テストコードを書いた上で内部実装の変更や機能の拡張を行おうというものである。
我々の班で、一つ失敗したと感じたのは、テストコードを適切に書かなかったために、後でバグが検出されたことである。
- まず我々の班では、「"データを書き込む"メソッドを利用してから"データを読み込む"メソッドを利用することで、データが正しく取り出せること」を単体テストとして記述し、処理が問題ないことを確認した。
- さてお題として出されたコードでは、データベースとして利用するファイルのファイル名が、コード中に定数として記述されていた。このため我々の班では、これを引数として与えられるように修正した。
→ そのときの単体テストとして追加したものは、「データベースアクセス用のインスタンスを、引数を与えて生成した場合、インスタンスに格納されたファイル名を正しく取り出せること」であった。 - またお題として出されたコードでは、「データベースのデータを取り出す/書き込むにあたって、毎回ファイルアクセスを行っている」という実装になっていた。このため我々の班では、「データベースを開いた際に、ファイル中のデータをすべてメモリ上に格納する」「データベースを閉じる際のみに、メモリ上のデータをファイルに書き込む」という実装に変更することにした。
→ そのときの単体テストとして追加したものは、「データベースを開いてから変更を加えて閉じ、改めて開くと、閉じたときの状態が復元される」であった。
→ しかし、他の単体テストが通る一方で、いま追加した単体テストだけは通らなかった。
→ 調べてみた結果、先程の「ファイル名を引数で与えられるようにする」修正において、ファイル名に「引数で与えたものを使う部分」と「元のコードのまま定数を使う部分」が混在していた(定数を使う部分を変換し忘れていた部分があった)ことが原因だった。
これは今考えると、2項目め(データベースとして利用するファイルのファイル名を、引数で与えられるようにする)を実装したときのテストケースが問題だったのであって、本来であれば、「データベースを開いてから変更を加えて閉じ、改めて開くと、閉じたときの状態が復元される」というテストをこの段階で書かなければならないのだろう。この場合、引数のファイル名を変えたことが、実際のファイルアクセスに反映されていることをテストする方が重要なので。
また一瞬困ったのは、テストコードを実行するとSegmentation Faultで落ちて、どこが問題なのか切り分けるのが大変だったこと。まあこれはC++だから仕方ないのですが…。@hotwatermorningさんがgdbで問題の箇所を調べて下さったので助かった。
…と面倒なこともありましたが、メンバー3人で「テストコードがあったおかげで、内部実装の変更を思い切って出来た!」ということを感じられて、楽しくTDDの実践を終えることができた。
また今回、TDDについて多くの重要なポイントが見えた。私がTwitterで実況したものを抜粋する。(主に、@t_wadaさんの講演内容や説明からです)
- "テストが命綱"、*緊急時に命綱を編むことはできない"
- 「自分が完璧なプログラマではないことを、TDDをすればするほどわかる」
- 「もし、テストコードなしで大幅な修正を加えるとしたら、信じられないほどのスキルと明確な理解が必要になる」
- 「静的言語の場合は、影響範囲を調べるためにコンパイルエラーを積極的に使う」「動的言語の場合はそれが使えないというデメリットはある。一方で、継ぎ目(コードを介入させる場所)を作りやすいというメリットがある」
- 「技術書の「写経」の方法。 1.ローカルで使える SCM を用意 2.「ほんたった」などで対象の本を固定 3.ひたすらサンプルコードを写して実行 4.実行するたびにコミット(コミットログにページ番号を含める) 5.疑問点があったらコミットログや本に書き込む 6.章ごとにタグを打つ」
- 「TDDは練習することで上手くなる(=才能ではない)」
- 乱数を含むコードに対するテストの書き方。(1)乱数の選択肢が1つしかない場合のテストを書くことで、乱数の影響以外の部分をテストする。(2)乱数のパターンを固定したテストを書きテストする。
- Rubyチームは2つとも三脚の足を揃えられていた(バージョン管理、テスティング、自動化)。文化とツールの充実を感じる。と絶賛してみる(@sandinistさん)
特に今回実習して、上記のうち「TDDは練習することで上手くなる(=才能ではない)」というのが重要なのかと感じた。「こういう場合にはこういうコードを書けばよい」というのを学べば学ぶほど、適切な単体テストが書けるようになるのだな、と感じられた。
最後に、主催の@shuji_w6eさん、北海道に来て下さった@t_wadaさん、拙作のC++コードにお付き合い頂いた@hotwatermorningさん・@asari755さん、またその他皆様ありがとうございました。
おまけ:自己紹介LT資料
(*1)例えば「// ここはコメントです[LF]x = y + z;」のようなコードを書くと、VisualStudioのエディタは[LF]を改行と扱う一方で、コンパイラ(厳密にはプリプロセッサ)は改行がないものと扱うのである。困った仕様です。
2011年05月21日
前日夜に@ayako119さんに誘われたので参加。
これはTDD(Test-Driven Development、テスト駆動開発)を実習形式で学ぶ会で、札幌での開催はこれが2回目である。私は初参加。
テスト駆動開発というのは簡単に言うと、「実際に必要なプログラムに求められる仕様をコード(単体テスト)として先に書き、それに対応した実装のみを行う、というのを細かく繰り返す」ことで、仕様に沿ったプログラムを書き上げていく、ソフトウェア開発の一手法である。
より詳しい解説は
連載:[動画で解説]和田卓人の“テスト駆動開発”講座|gihyo.jp … 技術評論社
をご覧下さい。
以下では、テスト駆動開発の基本をご存知の方向けに話を進める。
今まで、私は勉強会などでテスト駆動開発について話を聞いており、独学で実践していた。
ただ、単純なロジックに対するテストコードは難なく書けるようになってきたのだが、困っていたこととして、「コードとして書くことが難しい」仕様についてどう対処するか、という実践的な知識が足りてないと感じていたのである。
(ここで「プログラムのコードとして書くことが難しい」仕様として代表的なケースは、その仕様が「CPU利用率」や「現在時刻」など、「動作しているコンピュータの状態によって決まり、かつ自力での制御が難しい」ものに依存している場合である。)
今回のTDD Bootcampで出たこれに対する答えは、「そのテストしにくい部分だけ切り分ける」というものであった。
例として、Ruby向けの単体テストツール「RSpec」を用いて、「MyTimerクラスのインスタンスは、自身が生成された時刻を保持している」という仕様をテストとして書くことを考える。単純に進めれば
# テストコード
require "./mytimer"
describe MyTimer do
it "should contain its own created time" do
mt = MyTimer.new
# 以下が仕様(JUnitの「assertEquals」に相当する部分)。
mt.created_time.should == Time.now
end
end
とテストコードを書き、その後実際のコードを書き進めて最終的に
# 実際のコード
class MyTimer
def initialize # Rubyのコンストラクタは「initialize」というメソッドに記述する
@created_time = Time.now
end
attr_reader :created_time
end
となるであろう。
ただこれは問題があって、インスタンス生成時と実際のテスト時に時刻の秒の値が変わったときだけテストが「失敗」と判断され、それ以外の場合は「成功」と判断されてしまう。
これに対する対処法として、今回は「テストしにくい部分を切り分け、テストの際のみ別のものに差し替える」という方法が述べられていた。まず実際のコードにて、時刻を取得する部分を切り分ける。
# 実際のコード
class MyTimer
def initialize
@created_time = time_now
end
attr_reader :created_time
# 時刻を取得する部分だけ切り分け
def time_now
Time.now
end
end
そしてテストコードを実行する際には、この切り分けた部分だけ差し替えて実行する。例えばRubyなら
# テストコード
require "./mytimer"
describe MyTimer do
it "should contain its own created time" do
$current_time = Time.now # 本当はグローバル変数なんて使いたくないんだけど
# ここで、MyTimerを複製してテスト専用のクラスを作る
testclass_mytimer = MyTimer.dup
# そしてテスト専用のクラスのみ、さっき切り分けた部分を差し替える
testclass_mytimer.class_eval{ def time_now; $current_time; end }
mt = testclass_mytimer.new
mt.created_time.should == $current_time
$current_time = nil # 後始末
end
end
とする。
あと他に興味深かったのは、テストコードを「SetUp」「Exercise」「Verify」「TearDown」の4つに区分する、というものであった。これはテストコードを読みやすくする(=後で読んだり、他の人が読んでも分かりやすくする)ためのものであって、さっきの例でいえば
# テストコード
require "./mytimer"
describe MyTimer do
it "should contain its own created time" do
# ----- setup(下準備をする)
$current_time = Time.now
testclass_mytimer = MyTimer.dup
testclass_mytimer.class_eval{ def time_now; $current_time; end }
# ----- exercise(実際に挙動を確かめたい処理を実行する)
mt = testclass_mytimer.new # 今回は「インスタンスを生成したとき」の挙動を確かめたいので
# ----- verify(結果が期待されていたものであるか確かめる)
mt.created_time.should == $current_time
# ----- teardown(必要ならば、テストに使ったオブジェクト等の後始末をする)
$current_time = nil
end
end
のように切り分けることになる。
今後の自分のためになった一日になったとともに、今後も単体テストを積極的に活用しようという気持ちになれた。企画の@shuji_w6eさん、その他参加者の皆様、ありがとうございました。
【宣伝】
きたる6/11(土)のオープンソースカンファレンス2011 Hokkaidoにて、「札幌C++勉強会」として出展予定です。私はC++における単体テストツール「CppUnit」について記事を書いています。