SyncStitchを使ったAlternating bit protocol (ABP) の設計(3)〜歪みに対応する〜

前回までは通信路に歪みがないことを仮定してましたが、今回から通信路に歪みを考慮に入れます。つまり、送信した値と異なる値が相手に届くことがあると状況をつくります。

チャネルのモデリング

これまではSyncStitchのチャネルをそのまま使用していましたが、歪みを入れられるように自分でモデル化します。最初は歪みのないチャネルをモデル化して、今までと同じ検証結果が得られるか確認します。

受信者用のチャネルは以下のように定義します。send_to_r.0 と send_to_r.1 はそれぞれ受信者への 0, 1 を送信を要求するイベントです。send_to_r.0 の後、受信者が実際に受信するチャネル rch へ 0 または 1 を送信します。現時点では歪みを入れないので、send_to_r.0 の後の RC1 状態からは rch!0 の遷移しかまだありません。


図1. 受信者用のチャネル

送信者用のチャネルも同様に定義します。

図2. 送信者用のチャネル

最後にSYSを以下のように修正します。まず、Sender(のS0状態)と受信者のチャネル(の状態RC0)を合成し、send_to_r.0, send_to_r.1 を隠蔽します。send_to_r.0, 1 を隠蔽するのはそれが Sender と 受信者のチャネルの間のみやり取りされるイベントだからです。それとReceiver(のR0状態)と送信者のチャネル(の状態SC0)を同様に合成したプロセスと合成し、チャネル通信イベントを隠蔽します。

(define-process SYS
  (hpar (list sch rch)
     (hpar (list send_to_r.0 send_to_r.1) S0 RC0)
     (hpar (list send_to_s.0 send_to_s.1) R0 SC0)))

ここまで出来たら前回と同様に検証します。検査式は前回のままです。検証するとすべて通る事が確認できます。3つとも問題なく通れば、チャネルのモデル化に問題ないと言えるでしょう。これは言わば、リファクタリングしてテストを通すのと同じですね。

ここまでのモデルは https://github.com/masateruk/blog/blob/master/20130831/abp-2.ss にあります。

歪みをモデル化

ここまで出来てようやく歪みをモデルに入れていきます。ここでは通信路による歪みはチェックサムなどの誤り検出技術により検出できると仮定して、値 e が送られるとします。状態遷移で表すには、歪みの結果 e を送信する場合とそのまま送れる場合の2つの遷移を非決定的に組み合わせます。状態遷移図は次の通りです。


図3. 歪みを含む受信者用のチャネル

いったん、二つのtau遷移で二つの状態に別れてからそれぞれの値を送信します。tau遷移は外から観測できない内部遷移であり、今のように複数のtau遷移を用いると非決定的な状態遷移を表す事が出来ます。送信者用のチャネルも同様の修正を施します。

チャネルに歪みの可能性を入れたら他を変えずに検査をしてみます。するとデッドロック検査で違反が見つかります。右クリックで「explore」を選ぶとどこで止まっているか確認できます。


図4. デッドロック状態

図を見ると歪みが起きて受信用チャネルが RC4 で本来 0 を送るべきところを歪みの結果、e を送信しようとしています。一方、受信者は R0 で 0 を待っているためデッドロックが起きています。このデッドロックを含むモデルは https://github.com/masateruk/blog/blob/master/20130831/abp-3.ss にあります。

では、直しましょう。受信者は期待している値と異なる値を受信した場合は、ひとつ前に受け取った値を送信者に返信するようにします。一方、送信者は自分が送った値が返ってくるのを待ちますが、期待していたものと異なる場合は先に送った値を再送信します。それぞれの状態遷移図は以下のようになります。


図5. 歪みに対応した受信者の状態遷移図


図6. 歪みに対応した送信者の状態遷移図

ではもう一度検証してみましょう。今度は無事通りました。

発散

無事検査が通ったので、めでたし、めでたし、これで終わり。と行きたいところですが、そうはいかないのです。

実はこのモデルには問題があるのです。それを確認するためにひとつ検査を追加してみます。

(divergence SYS)

検査すると通らないことがわかります。この検査は何をやっているかと言うと、発散がないかどうか検査しているのです。発散とは、循環したtau遷移のことです。つまり、発散があるとtau遷移だけを延々と繰り返すことがあるのです。tau遷移は内部遷移であり、外からは観測できないので外から見ると何だか動いているようだが何ら仕事をしてないように見えます。いわゆるライブロックと呼ばれるものです。デッドロックと同様「explore」メニューから問題の遷移を見ると、歪みが置き続けて再送信が延々と繰り返されている事が分かります。

SYSに発散があるだけでもまずいのですが、さらに悪いことに発散があると拒否検証の結果は意味をなさないのです。よって、発散を除いてから拒否検証を行わないといけないのです。

ということで次回はこの発散を除く方法を紹介したいと思います。

最後のモデルは、https://github.com/masateruk/blog/blob/master/20130831/abp-4.ss にあります。

SyncStitchを使ったAlternating bit protocol (ABP) の設計(2)〜仕様と実装の比較〜

前回は通信路に歪みがないと仮定して、その上で 0, 1 をやり取りする送信者と受信者をモデル化してデッドロック検査を行いました。今回は仕様を状態遷移図で表し、その仕様との比較を試してみます。

さて、その仕様ですが、今回のABPの設計で確認したい事は「送信側から 0 を送信したら受信側に 0 が到達し、1 を送信したら 1 が到達する」です。この送信側の送信から受信側での到達の間、何回かのメッセージのやり取りがあるかもしれませんが、それは気にせずとにかく 0 を送ったら 0、1 を送ったら 1 となってほしいのです。

これを状態遷移図で表します。send.0, send.1 はそれぞれ送信側で 0, 1 の送信を開始するイベント、recv.0, recv.1 はそれぞれ受信側で待っていた 0, 1 を受信した通知のイベントです。


図1. 仕様の状態遷移図

上記仕様と比較するためにSenderとReceiverの定義を少し直します。今まではSenderは 0 および 1 の送信を(受信者の準備ができたら)自発的に行っていましたが、それぞれ開始のイベント send.0, send.1 で送信を始めるようにします。送信者の状態遷移図は以下のようになります。

図2. 送信者の状態遷移図

受信者は受信待ちになっているビットを受信したら通知のイベント recv.0, recv.1 を発行するようにします。これにより、受信者の状態遷移図は以下のようになります。

図2. 受信者の状態遷移図

最後にSYSプロセスの定義を以下のように修正します。

(define-process SYS
  (hpar (list sch rch) S0 R0))

前回は並行合成演算子 par を使いましたが、今回は隠蔽付き並行合成演算子hparを使用します。これによりSYSの外側からは rch, sch を使ったやり取りは見えません。逆に隠蔽されてない send.0 や recv.1 は観測可能です。よって、SYS プロセスで send.0 が観測できた後期待通り 0 が受信側に渡れば recv.0 が観測できるはずです。

ではSYSプロセスを検証しましょう。まずはデッドロック検査です。SYSを変更したのでもう一度デッドロック検査をしておきます。問題なく終了し、デッドロックがないことがわかります。次はいよいよ仕様とSYSの比較です。これにはトレース検証と拒否検証の二種類の検証があります。

トレース検証

詳しい定義はSyncStitchのユーザガイドを見てもらうとして一言で説明すると、トレース検証とは「仕様にない振る舞いをしないこと」を検証することです。仕様に書いていない良くない振る舞いをしない性質のことを安全性とも言うので、トレース検証とは安全性検査であるとも言えます。

SyncStitchでのトレース検証のやり方はAssertionsに (trace SPEC SYS) を追加してダブルクリックするだけです。検査すると緑色のチェックが表示され、トレース違反がないことが確認できます。

拒否検証

もうひとつの検証は、拒否検証です。トレース検証は仕様に書いていないことをしないことを検証する方法ですが、これだとSYSが何もしないとトレース違反がないことになります。実際にはこれは期待している事ではなく、仕様で定義する期待する振る舞いを実装がしてほしいものです。これを検査するのが拒否検証です。やはり正確な定義はユーザガイドを見てもらうとして、一言で説明すると拒否検証とは仕様が規定する振る舞いを実装が拒否しない、つまり、実装も同じように振る舞う事ができることを検証する事です。トレース検証の安全性と対比して、応答性の検査だと言われることがあります。

SyncStitchでの拒否検証のやり方はトレース検証と同様、Assertionsに (failure SPEC SYS) を追加してダブルクリックするだけです。検査すると緑色のチェックが表示され、拒否違反がないことが確認できます。

以上で、歪みがない通信路で 0, 1 を順番に送信する方法がわかりました(改めて検査するまでもないくらい簡単ですが)。
次回は、このモデルに歪みを入れていきます。

※ モデルは https://github.com/masateruk/blog/blob/master/20130816/abp-1.ss にあります。

SyncStitchを使ったAlternating bit protocol (ABP) の設計(1)〜シンプルなモデルの作成とデッドロック検査〜

以前いっしょに仕事していた先輩がSyncStitchという設計ツールを開発したので試用してみました。

SyncStitchの特徴は、状態遷移図で書いたモデルの振る舞いをシミュレーションしたり検証できたりします。さらに複数の状態遷移図を組み合わせときの振る舞いについても同様にシミュレーションと検証ができます。これによりマルチコアやマルチスレッドで動作するソフトウェアの再現性の低いバグを設計時につぶすことが期待できます。詳細はこちらの紹介文かこちらからダウンロードできるユーザガイドを見てください。

今回はこのSyncStitchを使ってAlternating bit protocol (ABP) の設計をしてみました。ABPは歪みのある通信路で 0 と 1 を交互に送信するためのプロトコルです。

最初は簡単なモデルから

こういった設計の基本はいきなり全部入りの複雑なものから始めるのではなく、単純なモデルから始める事です。そこで、最初は歪みのないモデルからスタートします。

まずどうやってモデル化するかの大雑把な方針を決めます。SyncStitchは動作する主体をプロセス、プロセス間の通信にはイベントもしくはチャネル通信を使ってモデル化します。まず送信者と受信者をそれぞれプロセスとして定義すると決めます。これは良いでしょう。次に送信者と受信者の間の通信路ですが、最初は歪みがないことを仮定していますのでSyncStitchのチャネルをそのまま使用する事にします。あとで歪みを考慮に入れるときには、チャネルをそのまま使ってはうまくモデル化できませんが今の段階では確実に通信が行われると仮定しているのでチャネルをそのまま使うことにします。チャネルは通信路の方向ごとに用意します。送信者から受信者への通信路を rch チャネル、受信者から送信者への通信路を sch チャネルとします。

方針がきまったらそれぞれを定義していきます。

まずは送信者を状態遷移図で定義します。初期状態をS0として、0 を送ったらS1状態へ遷移します。rch は受信者プロセスへ送るためのチャネルです。通信路に歪みがなければ、受信者から送信者のチャネル sch に 0 が返ってくるはずです。S1状態はその 0 を待っている状態です。S1 状態で 0 を受け取ると受信者が 0 を受信したことが確認できたので 1 を送信するS2状態に遷移します。S2状態で 1 を送信するとS3状態に遷移し 1 を待ち、1 を受信すると最初のS0に戻って再び 0 の送信を行います。以上が送信者の振る舞いです。


図1. 送信者の状態遷移図

受信者は、送信者を鏡移しにしたような振る舞いです。初期状態R0では 0 を待ちます。0 を受け取ったらR1へ。0 を返信したらR2へ遷移して 1 を待ちます。1 を受け取ったらR3へ遷移して 1 を返信して最初のR0に戻ります。

図2. 受信者の状態遷移図

最後に送信者と受信者が協調して動くシステムプロセスを並行合成演算子parを使って定義します。"並行合成演算子"は並行に動作する複数のプロセスからなるプロセスを定義する演算子です。(list sch rch)は同期リストと呼ばれ、プロセス間で同期するイベントです。チャネルを同期リストに指定した場合はそのチャネルを介した通信イベントをすべて指定したことになります。

(define-process SYS
    (par (list sch rch) S0 R0))

ここまでできたらシミュレーションをしてみます。プロセスリストからSYSを選択して右クリックして「explore」を選択。プロセス式の入力を求められたら、「SYS」と入れます。すると、プロセスの初期状態が表示され、左側に遷移リストが表示されます。遷移リストを選択すると、次の状態が表示されるので順番に遷移を選択する事で正しくモデル化できているか確認する事ができます。


図3. SYSのシミュレーション画面

ある程度シミュレーションをして正しそうだと確信したら、次は検証を行います。まずは一番簡単で手軽なデッドロックがないことを検査します。チャネル通信は同期通信ですので、もし送信とペアになる受信の遷移がなければプロセスは止まってしまいます。今回は受信者、送信者ともに一本道の遷移ですので遷移のガード条件や送信パラメータに誤りがなければデッドロックはないはずです。デッドロック検査はプロセスリストのAssertionsボタンを押して表示されるAssertionsウィンドウで(deadlock SYS)と入力してダブルクリックすることで検査できます。実行するとすぐに緑色のチェックが出てデッドロックがないことが分かりました。これでモデル化に単純なミスがないことが確認できました。

以上が最初のモデルの作成とデッドロック検査のやり方です。今回紹介したデッドロック検査は強力ですが、デッドロックがないことだけでは検証が十分とは言えません。そこで、次回はSYSプロセスと仕様を比較することで、SYSが仕様を満たしているかどうか検証する方法を紹介します。

今回のモデルは https://github.com/masateruk/blog/blob/master/20130813/abp-0.ss にアップしてあります。

(続)OCamlをC言語に変換するプログラムつくりました

OCamlで書いてC言語に変換するプログラム、最低限入れたかった機能を実装して一応形になりました(ソースコードこちら)。プライベートな時間をちまちま使って作ったので思ったより時間がかかってしまいました。最初にブログで紹介してから1年半もたちました。

OCamlをC言語に変換するプログラムをつくりました - masaterukの日記

サポートしている基本型は、Int型とBool型のみ。レコード型とバリアント型も使えます。型変数による多相型も定義できます。リスト型だけ組み込みでサポートしました(ただしライブラリはありません)。

関数を定義するときには、再帰関数でなくてもlet recとするのはMinCamlと同様です。ラムダは書けませんが、クロージャはサポートしてます。matchによるパターンマッチをサポートしているので、だいぶ書けるものが増えました。

たとえば、Haskellの例でよく引き合いに出されるクイックソートなら以下のように書けます。

let rec iter f xs =
  match xs with
  | [] -> ()
  | x :: xs -> f x; iter f xs

let rec append xs ys =
  match xs with
  | [] -> ys
  | x :: xs -> x :: (append xs ys)

let rec filter p xs =
  match xs with
  | [] -> []
  | x :: xs -> if (p x) then x :: (filter p xs) else (filter p xs)

let rec sort xs =
  match xs with
  | [] -> []
  | x :: xs ->
      let rec le y = (y <= x) in
      let rec gt y = (y > x) in
      let left = sort (filter le xs) in
      let right = sort (filter gt xs) in
      append left (x :: right)

let rec is_sorted xs =
  match xs with
  | [] -> true
  | [x] -> true
  | x :: y :: zs -> (x <= y) && (is_sorted (y::zs))

let () =
  let xs = [5; 2; 3] in
  let ys = sort xs in
  assert (is_sorted ys);
  iter print_int ys

通常のOCamlとの違いは、Listのライブラリがないので自前でリスト関数を実装していることと比較関数をラムダではなく名前をつけて関数を定義しているところくらいでしょうか。

破壊的代入をサポートしていないので書けるものがすこし限定されますが、プログラムを副作用が必要な部分とそうでない部分にわければ、副作用がないところは関数型でささっと書いてしまうことができます。テストプログラムとかプロトタイプとか使える用途はそれなりにあるんじゃないと期待してます。

ちなみに上のクイックソートOCamlでは37行ですが、C言語に変換すると361行になります。およそ10倍です。機械生成なので人間が書いたらもう少し短くなるでしょうが、それでもOCamlの方がずっと短く書けるでしょう。C言語の方が行数が多くなるのは、多相型を解決するための型変換とメモリ管理のためにポインタの参照カウンタ操作のコードが増えるからです。OCamlではコンパイラが自動で解決してくれる問題もC言語では自分で解決しなくてはなりません。メモリ管理とか人手でやってやれないことはないですが、ミスなく実装するのは相当難しいとツールで生成されるコードを見ると感じます。

C言語の方が速度が速いでしょうが、もうすべてをC言語で書くのはやめた方がいいでしょう。よくパフォーマンス改善は測定してからと言いますが、それなら最初はOCamlで書いて測定して改善が必要なところだけC言語だけで書き直した方がいいんじゃないでしょうか。

わたしが生産性を計測する理由

 私は5年ほど前から、仕事に費やす時間を計測している。何に、何分費やしたか?をすべて記録している。その記録を見返せば何時何分には何をやっていたのかを知る事ができる。私がこのような計測をしているにはねらいがあって、それは、

  1. 計測データを蓄積する事で、そのデータをもとにより正確な見積もりができるようになる
  2. 何に、何分かけているのかを見て反省する事でより効率的なプロセスを発見する

である。

さて、5年やった実績から先に言うと、残念ながらどちらも達成できていない。1 に関してはたまに見積もりのデータとして活用することはあるが、その結果どのくらい正確だったかはわからないし、2 についてはまったく効率的なプロセスの発見には至っていない(どころか発見の助けにもなっていない)。

なぜ上記ねらいが達成できてい理由は2つあげられる。ひとつは、

 a. 仕事の内容が過去と同じではない

である。ソフトウェア開発という仕事は新しいものを作る仕事であり、新規性がともなうのは避けられない。それでも過去に自分が行った仕事と似ているものであれば、そのときの実績値をもとに見積もりを出せば良いのだが、案外過去にやったものと似ている仕事は少ない。と言うか、仕事を始める時点では過去のものに似ているか?という判断ができるものが少ないし、似ていると思ったけどふたを開けてみるとだいぶ違っていた、ということもある。実際に似ているものがあったとしても条件が異なるため過去の実績値をそのまま見積もりにするわけには行かない。リスク分を見積もって何割か足したり、逆に経験した事で次ははまらないだろうと考えて何割か減らすということをして見積もりをだす。まったくの勘で見積もるよりはましなのかもしれないのだが、期待していたよりも過去の実績値がばっちり当てはまるということが少なかった(まあ見積もりが正確かどうかは分からないけど、過去の実績をもとに出してますっていうと見積もりの説得力が増すという効果はあったけど)。プロセス改善について言えば同じ作業を異なるプロセスで試す事ができないのでどちらがより効率的か?というのは単純には分からない。

そして、もうひとつの理由は、

 b. 仕事の規模を表す適当な単位が見つけられない

である。まあコーディングならばいろいろ妥協して行数とすることもできるんだろうけど、実際の仕事はコーディングだけではない。例えば、クラス構成を考えたり、既存のコードを調査したり、規格書を読んだり、開発環境を整えたり、といろんな仕事をする。そしてそれぞれの仕事の規模をあらわすうまい単位を見つけられないのである。そうすると、せっかく時間を計ってはいても作り出した成果の大きさが分からないので生産性を導きだす事ができない、というわけである。

以上のふたつの理由が大きく、私が当初たてた計測のねらいは達成できていない。では、計測する意味はまったくないのか?というとそういう訳でもなく、当初期待していなかった副次的な効果があった。そして今はこれが計測を続ける主要因となっている。それは、

計測すると集中力が増す

である。時間を計測するのでタスク名の横に開始時間を記録してから作業を始めるのだが、そうすると「今はこのタスクをやる時間」という意識が芽生えて他のことをしなくなる(たとえば無駄なメールチェックとか)。その結果、目の前のタスクに集中することができる。というか、目の前のタスクに集中してこなさないと残念な記録が残ってしまうのでついついがんばってしまう。今は見積もりの確度向上やプロセス改善はほとんどあきらめてしまったが、それでも計測をやめないのはこの集中力が増す効果によるところが大きい。

最後にわたしは読了していないけど、計測の定番書を参考文献として紹介して本稿を終える。

PSPガイドブック ソフトウェアエンジニア自己改善 (IT Architects' Archiveソフトウェア開発の課題)

PSPガイドブック ソフトウェアエンジニア自己改善 (IT Architects' Archiveソフトウェア開発の課題)

通勤時間を無駄にせず勉強時間に変えるライフハック術

忙しいエンジニアにとって電車での通勤時間は貴重な勉強時間。しかし、何気にかばんから取り出したiPhoneTwitterやらFacebookやらを眺めて気づけばもうすぐ会社に到着ってこと、たまにありません?

こうなるのを防ぐのにいい方法がないかなーっと先日会社で話していたところ、iPhoneを使った良いアイデアをもらいました。1ヶ月くらい試してなかなか良かったのでその方法を紹介します。

その方法とは「位置情報を使ってリマインダ設定をする」です。自分の場合は、家の最寄り駅から2駅目と会社の最寄り駅から2駅目に着いたら「読書」とリマインドするようにしました。TwitterFacebookはちょっとはチェックしたいので2駅目にしてます。

iOS標準のリマインダーを使う場合は以下のように設定します。

  1. 「+」ボタンでリマインダーと追加。
  2. 「読書」と入力(別に「読書」でなくても「勉強」でも何でもいいんですが)
  3. 追加したリマインダをタップして編集画面へ。
  4. 「指定した場所で通知」を「オン」にする。
  5. 住所のところをタップして、住所入力画面へ。
  6. 住所を入力をタップして、住所を入力。駅名をいれれば住所が入力されます。
  7. リマインダ編集画面に戻って「完了」

これで、指定した場所に着いたらリマインドされます。iPhoneでネットを見ている場合でも勉強するきっかけになるのでだらだらを時間を無駄にしてしまう事がなくなりました。

場所でリマインダ設定する場合、iPhoneの電池の減りが速いのではないかと危惧したのですが、自分の使用状況だとさほど違いは感じられません。

よかったらぜひ試してください。

オブジェクト指向プログラミングにおける単体テストのしかた

この記事は TDD Advent Calendar 2012 の最終日の記事です。前日の記事は@biacさんの「[コラム] テストファーストとは何か?: TDD.NET」、初日の記事は@sue445さんの「Try Dream Development : 夢の開発を始めよう #TddAdventJp - くりにっき」です。

TDD Advent Calendarの記事なのですが、TDDというより単体テストの話です。単体テストのやり方について人それぞれやり方があるかと思いますが、自分が単体テストをするときの手順をまとめてみました。以下がその手順です。

  1. テストするメソッドを決める。
  2. メソッドの仕様を確認する。
  3. 事前条件をいくつかに場合分けする。
  4. 上で分けた場合ごとに、テストケース用のメソッドをつくる。
  5. テスト対象のクラスのインスタンスを生成し、場合分けしたひとつの事前条件を満たすコードを追加する。
  6. テストメソッドを呼び出すコードを追加する。
  7. 戻り値をチェックするコードを追加する。
  8. 状態をチェックするコードを追加する。
  9. テストをビルドして実行する。
  10. テストが通らなければ修正する。
  11. まだテストしていない場合わけがあるときは、4に戻る。
  12. まだテストしていないメソッドがあるときは、1に戻る。

ポイントは「状態をチェックする」です。副作用があるメソッドの場合には、状態のチェックは欠かせません。たまに、戻り値のチェックのみをしているテストを見かけますが、必ず状態のチェックをしましょう。

以下では、例題を使って各手順の詳細を説明します。例題は、整数型を要素とするリストです。例題のソースコードは、https://github.com/masateruk/blog/tree/master/20121225 にあります。

class Node {
public:
    Node(int n);  /* コンストラクタ */
    int get();  /* 要素を取得する */
    Node* next();  /* 次のノードを取得する */
};

class List {
public:
    int length();  /* 要素数を取得する */
    Node* get_head();  /* 先頭のノードを取得する */
    void add_head(Node* n);  /* 先頭にノードを追加する */
};

1. テストするメソッドを決める。
 まずは、add_headのテストをすると決めたとしましょう。テストする時間が十分にあればすべてのメソッドをテストしましょう。時間が十分にとれないようだったら、バグが潜んでいる可能性が高そうなメソッド(たとえば、複雑であるとか、新規に追加した)をテストしましょう。

2. メソッドの仕様を確認する。
 テストの目的のひとつは、実装が仕様を満足しているかどうかを確認する事です。テストするには、対象の仕様を確認する事から始めます。仕様は事前条件と事後条件からなります。add_headの事前条件は「引数nは、NULLでないNode型のオブジェクト(へのポインタ)である」。事後条件は「事後のリストs'は、事前のリストsの先頭にノードnを追加したもの」です。

3. 事前条件をいくつかに場合分けする。
 事前条件を場合分けして、その場合ごとにテストします。場合分けするヒントは事後条件にあります。事後条件が「Aのとき〇〇、Bのとき〇〇、それ以外のとき〇〇」となっている場合は、それぞれA, B, それ以外を場合として列挙します。事後条件の表現にもよりますが、事前の状態に関しての場合分けとメソッドのパラメータについての場合分けを行い、それぞれの組み合わせから事後条件で表現にある場合をつくるのが良いと思います。add_headの場合、事後条件には事前の状態およびパラメータに関する事前条件が特にないので、事後条件をもとにすると場合分け数はひとつになります。
 ブラックボックステストでは事後条件をもとに場合分けをするのが基本ですが、それに加えて実装に分岐がありそうなもので場合分けをすると有効なテストとなります。add_headでは、(i) 空リストの場合と (ii) 空リストでない場合に分けることにします。

4. 上で分けた場合ごとに、テストケース用のメソッドをつくる。
 場合ごとにテストケースを書きます。さっそくadd_headの (i) 空リストの場合のテストケースを書いてみましょう。私はひとつのテストケースに対してひとつのメソッドを割り当てます。

void test_add_head_when_list_is_empty()
{
}

5. テスト対象のクラスのインスタンスを生成し、場合分けしたひとつの事前条件を満たすコードを追加する。
 続けてテストメソッドの中身を書いていきます。Listオブジェクトは初期状態では空リストなので単にオブジェクトを生成するだけです。

void test_add_head_when_list_is_empty()
{
    List l;
}

6. テストメソッドを呼び出すコードを追加する。
 引数で渡すNodeオブジェクトを作ってadd_headを呼び出します。

void test_add_head_when_list_is_empty()
{
    List l;
    Node n(11);

    l.add_head(&n);
}

7. 戻り値をチェックするコードを追加する。
 add_headは戻り値を返さないので、ここでは何も追記しません。

8. 状態をチェックするコードを追加する。
 ここが重要なポイントです。事後条件によると、リストの先頭に要素が追加されているはずなので、それらをチェックします。

void test_add_head_when_list_is_empty()
{
    List l;
    Node n(11);

    l.add_head(&n);

    // 長さのチェック --- (a)
    TEST_ASSERT(l.length() == 1);
    // add_headで追加した要素が確かに取得できる --- (b)
    TEST_ASSERT(l.get_head()->get() == 11);
    // 11の要素しかふくまれないことを確認するassertion --- (c)
    TEST_ASSERT(l.lget_head()->next() == NULL);
}

 状態のチェックは利用できるメソッドをすべて使って行います。(c)のassertは、add_head()によって11以外の余計な要素が含まれていないを確認するためのものです。例えば、(a), (b) しかないと以下のような実装もテストが通ります。

class List {
public:
    int length();
    Node* get_head();
    void add_head(Node* n);

private:
    int m_length;
    Node* m_head;
};

List::List()
    : m_length(0), m_head(NULL)
{
}

int List::length()
{
    return m_length;
}

Node* List::get_head()
{
    return m_head;
}

void List::add_head(Node* n)
{
    m_length++;
    m_head = new Node(n->get()); // わざと余計な要素を追加
    m_head->m_next = n;
}

かなりわざとらしいですが、(c)がなければ上記のようなノードをひとつ余計に追加する実装でもテストが通ってしまいます。よって、状態のチェックは利用できるすべてのメソッドで確認することが重要です。

9. テストをビルドして実行する。
 テストメソッドを書いたらビルドして実行します。

10. テストが通らなければ修正する。
 テストが通らない場合は修正します。テスト駆動開発ではこの段階で実装を始めます。

11. まだテストしていない場合わけがあるときは、4に戻る。
 3で列挙した場合わけが残っている場合は、そのテストも同様の手順で書きます。(ii) 空リストでない場合のテストは以下のようになります。

void test_add_head_when_list_is_not_empty()
{
    List l;
    Node n1(11), n2(12), n3(13);

    l.add_head(&n1);
    l.add_head(&n2);

    l.add_head(&n3);

    TEST_ASSERT(l.length() == 3);
    TEST_ASSERT(l.get_head()->get() == 13);

    // 以下はadd_head後も先に入っていた12, 11が変わらないことを確認するassertion -- (d)
    TEST_ASSERT(l.get_head()->next()->get() == 12);
    TEST_ASSERT(l.get_head()->next()->next()->get() == 11);
    TEST_ASSERT(l.get_head()->next()->next()->next() == NULL);
}

ここでも(d)が重要な役割を果たします。もし(d)がなければ以下のような実装がテストを通ってしまいます。

void List::add_head(Node* n)
{
    m_length++;
    m_head = n;
}

テストしているメソッドにより変化を起こす状態をチェックするとともに、変化してはいけないものが変化していないことも漏れなく確認しましょう。

12. まだテストしていないメソッドがあるときは、1に戻る。
 同じ手順を他のメソッドについて行います。すべてのメソッドをテストしたら終了です。

すべてのメソッドをテストしたら終了です。最後によく聞かれる質問とその回答をふたつあげておきます。

Q1. 事前条件を満たさないテストはしますか?
 しません。なぜなら事前条件を満たさない呼び出しでは何も保証しないからです。よって、テストを書く呼び出し側からすれば、エラーを返ってくることを期待できません。アサートで落とされたりするかもしれません。さらに呼び出しから返ってこないかもしれません。以上のことから、メソッド呼び出し後の期待値を書けません。よってテストしません。

Q2. 情報取得のメソッドが不足していてチェックしたい情報がそろわないときはどうしますか?
 情報取得のメソッドが十分にそろっていないときは、副作用のあるメソッドを使って仕様である状態遷移に従って遷移できるかどうか確認します。

以上、オブジェクト指向プログラミングにおける単体テストの仕方を私なりのまとめてみました。これは契約による設計(Design by Contract : DbC)を学んで自分なりに考えたやり方です。契約による設計を基礎から学びたい方は下の本がおすすめです。

オブジェクト指向入門 第2版 原則・コンセプト (IT Architect’Archive クラシックモダン・コンピューティング)

オブジェクト指向入門 第2版 原則・コンセプト (IT Architect’Archive クラシックモダン・コンピューティング)

最終日の記事なので、TDD Advent Calendarを総括するような記事が期待されたかもしれませんが、普通にテストに関する記事にさせていただきました。最終日の記事を練っていた方もいたかと思いますが、快く担当させていただき感謝しています。ありがとうございます。

では、最後に。

メリークリスマス