少し前ですが、TDD/BDD入門連載の第一回を執筆させていただきました。
http://www.atmarkit.co.jp/ait/articles/1403/05/news035.html
入門連載の最初ということで基本的な内容が中心です。第二回以降や関連連載でも、テスト自動化研究会の仲間による応用的・実例的な解説が続いていく予定ですのでご期待ください。
少し前ですが、TDD/BDD入門連載の第一回を執筆させていただきました。
http://www.atmarkit.co.jp/ait/articles/1403/05/news035.html
入門連載の最初ということで基本的な内容が中心です。第二回以降や関連連載でも、テスト自動化研究会の仲間による応用的・実例的な解説が続いていく予定ですのでご期待ください。
CやC++の開発ではメモリリークに悩まされることが多い。メモリ管理はスマートポインタに限定するなど自分たちが注意しても、外部で開発されたコードやレガシーコードによって結局逃れられないことがしばしばある。
さらに組み込み開発といったコードの実行環境に制約が多い場合は、検出や再現がやりにくいことから、メモリリークのデバッグやテストが結構なストレスになることがある。
こうした、面倒な問題になりがちなメモリリーク対応では、全てに対応できるというわけではないけれど、ユニットテストでの検証が有効なことが多い。ユニットテストならば、再現性の確保、異常な入力の実現、コードの切り分けといったものが容易なためだ。デバッグ等で便利なので、今回いくつかの方法をまとめたいと思う。
今回はメモリリークを発生させる題材として、以下のコードを解析する。
class Base { }; class Hoge : public Base { int *pvalue_; public: Hoge() { pvalue_ = new int; } ~Hoge() { delete pvalue_; } };
Effective C++など数多くのC++の解説書が注意していて有名だけれど、上記のHogeクラスを以下のように使用するとメモリリークが発生する。
Base *pHoge = new Hoge(); delete pHoge;
ユニットテストでメモリリークを検出する方法としては、まず単純にメモリリークを検出する動的解析ツール(例えばValgrind等)上でユニットテストを実行させる方法がある。
例えば以下の様なgoogle testのテストコードを記述する。
TEST(HogeTest, hoge) { //下記コードでメモリリークが発生 Base*pHoge = new Hoge(); delete pHoge; };
これをビルドして、以下のようにValgrind上で動かしてしまう(test_runner.outがテストの実行ファイル)。
valgrind --tool=memcheck --leak-check=yes --leak-resolution=high ./test_runner.out
すると、以下のようにメモリリーク発生をValgrindが報告してくれる。
==5155== 4 bytes in 1 blocks are definitely lost in loss record 1 of 1 ==5155== at 0x402B9B4: operator new(unsigned int) (in /usr/lib/valgrind/vgpreload_memcheck-x86-linux.so) ==5155== by 0x804AE38: Hoge::Hoge() (main.cpp:17) ==5155== by 0x804AC71: HogeTest_hoge_Test::TestBody() (main.cpp:29) (中略) ==5155== LEAK SUMMARY: ==5155== definitely lost: 4 bytes in 1 blocks ==5155== indirectly lost: 0 bytes in 0 blocks ==5155== possibly lost: 0 bytes in 0 blocks ==5155== still reachable: 0 bytes in 0 blocks ==5155== suppressed: 0 bytes in 0 blocks
この方法の良い所としては以下がある。
ただ欠点もある。まずCやC++のテストフレームワークはプリプロセッサで関数名を生成することが多いので、動的解析ツールのレポートからどのテストメソッドでリークが発生したかの判別がしばしば面倒になる。またValgrindとユニットテストのログが混在してしまうので、ログの切り分けや用途の切り分けが必要になる。
ただ一方で動的解析ツールで問題として挙げられがちな、処理の低速化やタイミングのズレについては、ユニットテストならば問題にならないことが多い。
なお何が良いかは環境要因も多いだろうけれど、ユニットテストによるメモリリーク検出については、自分はとりあえず上記の例のようにValgrind上でgoogle testやCppUTestを動かす方法に頼ることが多い。
実行環境や処理系がメモリリークの検出機能を持つものがある。その活用でユニットテスト上でのリークの検出が可能な事がある。
例えばC++向けのユニットテストフレームワークであるBoost.Testは、MSVCでビルドして実行すると、MSVCの機能を用いてデフォルトでメモリリークの検出・報告を行ってくれる。
例えば以下の様なコードを実行する
#define BOOST_TEST_MAIN #include <boost/test/included/unit_test.hpp> #include "hoge.h" BOOST_AUTO_TEST_CASE(testHoge) { Base *pHoge = new Hoge(); delete pHoge; }
するとテストレポートに以下の様なメモリリークのレポートが挿入される
Detected memory leaks! Dumping objects -> (略)
こちらはテストコード上でメモリリーク検出機能を記述できるので、その他のユニットテストと統合しやすい・レポートも見やすいといったメリットがある。
前の方法と被るところもあるけれど、あとはmallocやnewなどのメモリ管理関数を、メモリリーク検証機能付きのものに置き換える方法がある。
例えば著名なmtraceはそれに該当する。あとGoogle Testのマニュアルでも、new、deleteを自前のものに置き換えてメモリリークを検証する例が紹介されている(sample10_unittest.cc - googletest - Google C++ Testing Framework - Google Project Hosting)。
この方法は、製品コードあるいはテストコードに専用のコードを記述する手間がかかる。ただ速度低下や環境制約などが少ないので、テスト環境・本番環境を区別せずに活用できるメリットが大きい。 例えばメモリリーク検出機能を持つ独自のアロケータを製品コードで使用しておけば、システムテスト中でもメモリリークの発生有無をチェックできるようになる。
先日、Cadence SMVによるモデル検査を扱う機会があった。アルゴリズムの検証で有用だと感じたので 復習がてら簡単に使ってみたい。
今回は、並行コンピューティング技法で紹介されているスピンロックのバグの実装例をモデル検査で検証する。
その検査対象の擬似コードは以下の通り。2つのスレッドがスピンロックでそれぞれのCriticalRegionの保護しながら動作する。当然ながらこのロックには欠陥がある。
int thread0inside = 0; int thread1inside = 0; void threadZero() { while (true) { while (thread1inside) {} ...OtherTask... thread0inside = 1; ...CriticalRegionOfThreadZero... thread0inside = 0; ...OtherTask... } } void threadOne() { while (true) { while (thread0inside) {} ...OtherTask... thread1inside = 1; ...CriticalRegionOfThreadOne... thread1inside = 0; ...OtherTask... } }
検査手段としては、冒頭のCadence SMVを用いる。
今回はあくまで例示であるため、CriticalRegionOfThreadZero、CriticalRegionOfThreadOneが同時実行されないことをモデル検査で検証する
SMVは状態遷移を記述する。各コードを抽象化して、スレッド内のプログラムカウンタ(もっと簡便に説明するなら行番号)の値を状態とし、プログラムカウンタが進んだりループしたりするのを状態遷移と見なす。
SMVのコードは次のようになる。
MODULE main VAR thread0inside : boolean; thread1inside : boolean; th0pc : 1..5; th1pc : 1..5; ASSIGN init(thread0inside) := FALSE; init(thread1inside) := FALSE; init(th0pc) := 1; init(th1pc) := 1; next(thread0inside) := case th0pc = 2 : TRUE; th0pc = 4 : FALSE; TRUE : thread0inside; esac; next(thread1inside) := case th1pc = 2 : TRUE; th1pc = 4 : FALSE; TRUE : thread1inside; esac; next(th0pc) := case th0pc = 1 & thread1inside = FALSE : {1, 2}; th0pc > 1 & th0pc < 5 : th0pc + 1; th0pc = 5 : {5, 1}; TRUE : th0pc; esac; next(th1pc) := case th1pc = 1 & thread0inside = FALSE : {1, 2}; th1pc > 1 & th1pc < 5 : th1pc + 1; th1pc = 5 : {5, 1}; TRUE : th1pc; esac; SPEC AG(!(th1pc = 3 & th0pc = 3))
th1pc、th0pcが各スレッドのプログラムカウンタで、1にスピンロックのスピン、2にロックのフラグ更新、3にCriticalRegionの処理、4にロックを解除するフラグ更新、5にCriticalRegionと無関係な処理があるとする。
th1pc、th0pcが同時に3になるとバグとなる。
上記のSMVのコードをCadense SMVのモデル検査器で検査すると、以下の様な出力結果が得られる。
(中略) Model checking results ====================== (AG $$$_mc_0)..............................................................false (中略)
これはスピンロックで保護しているはずのCriticalRegionOfThreadZero、CriticalRegionOfThreadOneが同時実行される欠陥があることを示している。
なおcadense SMV実行時に生成される「*.out」ファイルに、どのような遷移で同時実行状態になるかのパス情報が記述されている。
先日、Boost.勉強会 #14 東京に参加させて頂いたが、そこでBoost.Graphの話があった。
復習がてら、今回はTDD Boot Camp 東京 for C++での最短経路探索の課題(TDD Boot Camp 東京 for C++ 課題)をBoost.Graphで解いてみる。なお課題は規模が大きいので、今回は横浜近辺の5駅に絞った。
まずBoost.Graphでグラフ構造を記述する。
なおBoost.Graphはgraphvizのグラフ図を出力できるので、今回は記述したグラフ構造をgraphvizで画像化して視覚的に確認する。コードは以下。
#include <iostream> #include <vector> #include <boost/graph/adjacency_list.hpp> #include <fstream> #include <boost/graph/graphviz.hpp> using namespace boost; typedef adjacency_list<listS, vecS, undirectedS, no_property, property<edge_weight_t, int>> Graph; typedef std::pair<int, int> Edge; typedef graph_traits<Graph>::vertex_descriptor Vertex; //駅の定義 enum { Yokohama, MusashiKosugi, Kawasaki, Shibuya, Tokyo, N }; const char* NameList[] = { "Yokohama", "MusashiKosugi", "Kawasaki", "Shibuya", "Tokyo" }; int main() { //経路と所要時間の定義 const std::vector<Edge> edges = { {Yokohama, MusashiKosugi}, {Yokohama, Kawasaki}, {MusashiKosugi, Kawasaki}, {MusashiKosugi, Shibuya}, {Kawasaki, Tokyo}, {Shibuya, Tokyo} }; const std::vector<int> weights = { 23, 14, 19, 21, 24, 25 }; const Graph g(edges.begin(), edges.end(), weights.begin(), N); std::ofstream file("trainpath.dot"); write_graphviz(file, g, make_label_writer(NameList)); return 0; }
上記のコードを実行した後、生成されたgraphvizのdotファイルを「dot -Tpng trainpath.dot -o trainpath.png」などのコマンドでpngに変換する。すると以下の様な画像が得られる。
グラフ構造を記述できたら、今回は横浜・東京間の最短経路をダイクストラ法を使って導き出す。
コードは以下。
#include <iostream> #include <vector> #include <deque> #include <boost/graph/adjacency_list.hpp> #include <boost/graph/dijkstra_shortest_paths.hpp> using namespace boost; typedef adjacency_list<listS, vecS, undirectedS, no_property, property<edge_weight_t, int>> Graph; typedef std::pair<int, int> Edge; typedef graph_traits<Graph>::vertex_descriptor Vertex; //駅の定義 enum { Yokohama, MusashiKosugi, Kawasaki, Shibuya, Tokyo, N }; const char* NameList[] = { "Yokohama", "MusashiKosugi", "Kawasaki", "Shibuya", "Tokyo" }; int main() { //経路と所要時間 const std::vector<Edge> edges = { {Yokohama, MusashiKosugi}, {Yokohama, Kawasaki}, {MusashiKosugi, Kawasaki}, {MusashiKosugi, Shibuya}, {Kawasaki, Tokyo}, {Shibuya, Tokyo} }; const std::vector<int> weights = { 23, 14, 19, 21, 24, 25 }; //最短経路の探索 const Graph g(edges.begin(), edges.end(), weights.begin(), N); const Vertex from = Yokohama; const Vertex to = Tokyo; std::vector<Vertex> parents(num_vertices(g)); std::vector<std::size_t> distance(num_vertices(g)); dijkstra_shortest_paths(g, from, predecessor_map(&parents[0]).distance_map(&distance[0])); //探索結果をリスト化 std::deque<Vertex> route; for (Vertex v = to; v != from; v = parents[v]) { route.push_front(v); } route.push_front(from); for (const Vertex v : route) { std::cout << NameList[v] << std::endl; } return 0; }
これを実行すると最短経路が以下のようにリストアップされる。
Yokohama Kawasaki Tokyo
また最短経路の時間は、上記のコードの場合distance[to]に格納されている。また今回はグラフ構造が自明だったため今回は省いているけれど、経路が存在しないことは「parents[to] == to」が真となることの確認で判定できる。
先日、組込みソフトウェア技術研究会というところで、組込みでのテスト自動化について紹介をさせていただきました・
当日資料を下記に共有します。
https://drive.google.com/folderview?id=0BwzSv7bTjDW9VEJhRS13dVhMeHM&usp=sharing
先日、JaSST13'東海のポスターセッションにて、「テストの品質モデル構築の取り組み」と題した発表をさせて頂きました。
内容は、まず前提として、テスト環境・テストスクリプト・テストデータといった、テスト実装・テスト環境構築の成果物の総称を指す言葉として「テストシステム」という用語を定義しています。そしてそのテストシステムの品質モデルを、ISO/IEC25010ベースで作成したものが主なコンテンツとなっています。
背景ですが、現在では同じテスト環境が、プログラミング中・テスト工程・リリース作業等々、開発ライフサイクルを横断して活用されるのが(特にテスト自動化で顕著ですが)ありふれた姿となっています。
また特に自動テストについては、プロジェクトを超え、資産としてブランチや派生プロジェクトにも引き継がれるようになっています。そこでは例えば自動テストがTests as DocumentationやSpecification by exampleの実現手段として使われたり、仕様化テストやCover&Modifyの起点として活用されたりします。
なお、このように様々なステークホルダが様々な用途でテストを使うならば、テストの構築も書捨て・使い捨てでなく、求められる品質をきちんと特定して実現する必要性が出てきます。
例えばテストを継続的に活用するなら、保守性を高めないとテストの保守コストが悪化して身動きがとれなくなります。またプログラミングで構築したテストをそれなりに厳格な品質保証でも活用するなら、きちんとしたテストアーキテクチャの確保を行わないと、テスト設計の整合性維持や流用が困難になります。
今回の発表は、そういったテストに求められる品質の特定・確保をどうすればうまくこなせるか、という検討の活動の一環となっています。まだまだ要検討ですが、品質モデルは、そこで使えそうなツールの1つになれば、と思っています。
ソフトウェア品質モデルの国際的な標準規格としては、長らくISO/IEC 9126が一般的でした。ただ規格が置換されたこともあり、最近はISO/IEC 25000シリーズ(通称SQuaRE)が使われるようになっています。
ISO/IEC 25000シリーズでは、ISO/IEC 9126と比べていろいろ変更や拡張が行われています。特になかでも目を引くのがISO/IEC 25012でデータの品質モデルを新たに追加している点です。データの品質モデルはこれまでなかったものではないものの、システムの品質モデルと並ぶ位置づけで定義しているのが印象強いです。
このデータの品質モデルについてですが、上位のレベルのテスト設計・実装で活用できそうなものとなっています。
というのも、データ駆動テストやキーワード駆動テストを行うと、データ(キーワード含む)と、それ以外のスクリプトやフレームワーク部分で利用者・利用状況が異なる場合が出てくるためです。そうした場合では、データの品質モデルに基づいて、データやキーワードに要求される品質の抽出・分析・確保を行うとしばしば効果的です。
ISO/IEC 25012についてはまだまとまった情報が少ないようなので、今回はテストデータをターゲットと想定して、品質モデルの概要をまとめたいと思います。
なおISO/IEC 25012は、使っていて保守性についての品質モデルの定義が弱いと感じています。これについては運用の際に各自補強した方が良いかもしれません。
ISO/IEC 25012の品質モデルでは、15個程度の品質特性を定義している。それらは以下の「固有のデータ品質」「システム依存のデータ品質」の2つの属性でタグ付されている。
データが構文的・意味的に正しいかどうかの度合。
例えばデータがテストケース仕様が意図したデータとなっているか。データに誤字・脱字はないか等。
利用状況や目的に対して、属性や実態インスタンスを持っているかの度合。
例えばテスト十分性基準を満たすテストデータを用意できているか等。
データ間で矛盾がないか、全体整合がとれているかの度合。
利用時のデータの信用度。
例えばテストデータはレビューされたものか、信頼できるツールで生成されたものか等
利用時点でデータが最新かどうかの度合。
例えばテストで使用する際にデータが望ましい最新版となっているか等。
データや、データの利用手段となるシステムのアクセシビリティ(障害や制約を持つ人でも使いやすいものになっているか)
例えばシステムが弱視でも扱えるようにフォントサイズ変更機能を持っているか等。
データや、データの利用手段となるシステムを対象とする、標準的な規格や規則の順守度。
例えばシステムがCSVで認可されているか等。
データについて、承認された条件下でのみ利用可能であることの保証度。例えば暗号化されているか等。
またはデータの利用手段のシステムの機密性。例えば認証を通したときのみデータにアクセスできるか等。
データやデータの利用手段のシステムが、どれぐらい記憶容量や処理量といった資源を必要とするかの度合。
例えば全テストデータを用いたテスト実行時間等。
データやデータの利用手段のシステムが、どれぐらいの精度で利用可能かを示す度合。
例えばテストデータの有効桁数がどれぐらい大きいか、テストデータで表現できる最小値がどれぐらい小さいか等。
データやデータの利用手段のシステムが監査証跡をどの程度提供しているかの度合。
例えばデータの変更履歴や使用履歴を追えるか等。
データやデータの利用手段のシステムを対象とした場合の、利用者にとっての理解しやすさや説明しやすさ。
例えばテストデータの可読性等。
データの利用がどの程度できるか。
例えばテストデータに同時にアクセスできるか、自動ツールでもアクセスできるか等
データのシステム間の移植のしやすさ。
データの維持やバックアップのしやすさ。例えばデータのバックアップシステムがどの程度充実しているか等。