はじめに
初期ファイナル・ファンタジーの伝説的プログラマ、ナーシャ・ジベリの「早撃ち」エピソードを知っていますか? 彼はヒーローのようにさっそうと現れ、どんなプログラムでも電光石火で書いてしまいます。「見てくれの悪さは気にしねえ。誰よりも早くやってやるぜ」。やがていくつかの伝説を残し、彼もプロジェクトを去るときがやってきました。残った同僚たちは困りました。彼の超絶プログラムは彼にしか理解できず、バグがあっても修正できないのです。それに、変更しようとすると動かなくなってしまいます。「ナーシャ、カムバック!」でも彼はもう戻ってきません……。
こうした悲劇を防ぐ方法の1つがソフトウェアテストです。OpenFlowコントローラのように動作シーケンスが複雑なソフトウェアが壊れていないことを確認するためには、ソフトウェアにより自動化されたテストが有効です。それに、きちんとしたテスト一式があればプログラム本体の理解もしやすく、修正も簡単です。
TremaはOpenFlowコントローラをテストするためのテストツールが充実しています。今回はこれを使って、簡単なコントローラ(リピータハブ)をテストファースト形式で実装していきます。
ではさっそく実際の例を見ていきましょう。
リピータハブの設計
まずは、リピータハブがどのように動作するかを説明しましょう。ホスト3台のネットワークを考えてください。あるホストからパケットを送信すると、リピータハブは入ってきたパケットを複製してほかのすべてのホストにばらまきます。
OpenFlowプロトコル的に何が起こっているかを図1に示します。host1がパケットを送信すると、スイッチからコントローラにpacket_inが起こります。ここでコントローラは「今後は同様のパケットをほかの全ポートへばらまけ(FLOOD)」というflow_modを打ちます。また、packet_inを起こした最初のパケットをほかのすべてのホスト(host2 とhost3)に明示的にpacket_outで届けます。
最初のテスト
ではさっそく、リピータハブのテストコードを書いていきましょう。TremaのテストフレームワークはRubyのユニットテストツールRSpecと統合されています。まだインストールしていない人は、「geminstall rspec」でインストールしてください。また、TremaのAPIについては、こちらを参照してください。
テストコードの最初のバージョンはリスト1のとおりです(spec/repeater-hub_spec.rb)。最初の行は、テストに必要なTremaのライブラリを読み込みます。describeで始まるdo…endブロックはテストの本体で、RepeaterHubコントローラのふるまいをここに記述(describe)する、という意味です。
RepeaterHubを定義していないのでエラーになることはわかりきっていますが、テストを実行してみましょう。次のコマンドを実行すると、Tremaを起動したうえでspec/repeater-hub_spec.rb(リスト1)のテストを実行します。
予想どおり、定数RepeaterHubが未定義というエラーで失敗しました。エラーを修正するために、RepeaterHubクラスの定義を追加してみましょう(リスト2)。
本来、コントローラクラスは独立した.rbファイルに書きますが、今回は簡便さを優先し、テストコード上に直接書いているので注意してください。それでは実行してみましょう。今度はパスするはずです。
やった! これで最初のテストにパスしました。
このようにテストファーストでは、最初にテストを書き、わざとエラーを起こしてからそれを直すためのコードをちょっとだけ追加します。テストを実行した結果からのフィードバックを得ながら「テスト書く、コード書く」を何度もくりかえしつつ最終的な完成形に近づけていくのです。
パケット受信のテスト
では、リピータハブの動作をテストコードにしていきましょう。どんなテストシナリオが思いつくでしょうか? とりあえず、こんなのはどうでしょう。
「ホスト3台(host1、host2、host3)がスイッチにつながっているとき、宛先をhost2としたパケットをhost1が送ると、host2とhost3がパケットを受け取る。」
テストコードはリスト3のようにitブロックの中に記述します。
テストシナリオをテストコードに置き換えるには、シナリオの各ステップをGiven(前提条件)、When(○○したとき)、Then(こうなる)の3つに分解するとうまく整理できます。
- 【Given】ホスト3つ(host1、host2、host3)がスイッチにつながっているとき、
- 【When】host1がhost2にパケットを送ると、
- 【Then】host2とhost3がパケットを受け取る
では、Given、When、Thenの順にテストコードを書いていきます。
【Given】ネットワークの構成
シナリオの前提条件(Given)として、テストを実行するホスト3台のネットワーク構成(図1)をリスト4 のように定義します。
これはネットワーク設定とまったく同じ文法ですね! ここで、それぞれの仮想ホストがpromiscオプション(自分宛でないパケットを受け取る)を"on"にしていることに注意してください。リピータハブではパケットがすべてのポートにばらまかれるので、こうすることでどこからのパケットでも受信できるようにしておきます。
【When】パケットの送信
Whenは「○○したとき」というきっかけになる動作を記述します。ここでは、Givenで定義されたホストhost1からhost2にパケットを送ります。パケットを送るコマンドは、trema send_packetsでした。テストコード中でもこれに似たAPIを使うことができます(リスト5)。
run(RepeaterHub)は、Givenで定義されたネットワークの上でRepeaterHubコントローラを動かし、続くブロックを実行するという意味です。
【Then】受信パケット数のテスト
Thenには「最終的にこうなるはず」というテストを書きます。ここでは、「host2とhost3にパケットが1つずつ届くはず」を書けばよいですね(リスト6)。
vhost("ホスト名")は仮想ホストにアクセスするためのメソッドで、仮想ホストの受信パケットなどさまざまなデータを見ることができます。ここでは、受信したパケットの数、つまり受信パケットカウンタstats(:rx)が1ということをテストしています。
テストを実行
ではさっそく実行してみましょう。
失敗しました。「host2はパケットを1つ受信するはずが、0 個だった」というエラーです。RepeaterHubの中身をまだ実装していないので当たり前ですね。すぐにはなおせそうにないので、ひとまずこのテストは保留(pending)とし、あとで復活することにしましょう(リスト7)。
今度は実行結果が次のように変わり、エラーが出なくなります。
ここでの失敗の原因は、いきなりすべてを実装しようとしたことでした。以降では、リピータハブの動作を図1の①と②の2段階に分け、1つずつテストファーストで実装していくことにしましょう。
フローエントリのテスト
まずは、スイッチにフローエントリができることをテストしてみましょう(図1の①)。テストシナリオは次のようになります。
- 【Given】ホスト3つ(host1、host2、host3)がスイッチにつながっているとき、
- 【When】host1 がhost2 にパケットを送ると、
- 【Then】パケットをばらまくフローエントリをスイッチに追加する
では、これをテストコードにしてみましょう。GivenとWhenは最初のテストシナリオと同じで、Thenだけが異なります。パケットをばらまく処理はFLOODですのでリスト8のようになります。
ネットワーク構成のコード(network {…… }の部分)をコピペしてしまっていますが、あとできれいにするので気にしないでください。エラーになることを見越しつつ、さっそく実行すると、次のエラーになります。
「スイッチにフローエントリが1つあるはずがなかった」というエラーです。では、flow_modを打ち込むコードをRepeaterHubクラスに追加して、もう一度テストしてみましょう(リスト9)。
別のエラーになりました。「アクションが"FLOOD"でなく"drop"だった」と怒られています。たしかに、さきほどのflow_modにはアクションを設定していなかったので、デフォルトのアクションであるdrop(パケットを破棄する)になってしまっています。flow_modにパケットをばらまくアクションを定義してみましょう(リスト10)。
今度はテストが通りました! それでは、もう少しThenを詳細化し、フローの特徴を細かくテストしてみましょう(リスト11)。
ここではホストにIPアドレスを振り、フローのsrcとdstがこのアドレスに正しく設定されているかをチェックしています。実行してみましょう。
失敗しました。フローの srcには、パケット送信元であるhost1 のIP アドレス192.168.0.1がセットされるべきですが、何もセットされていません。では、fl ow_mod で:match を指定して、この値がセットされるようにします(リスト12)。
テストにパスしました! これで、フローエントリが正しくスイッチに書き込まれていることまで(図1の①)をテストできました。
テストコードのリファクタリング
テストが通ったので、最後にコードの重複部分をまとめておきましょう。同じnetwork{ ……}が重複しているので、aroundブロックを使って1箇所にまとめます(リスト13)。
再びパケット受信のテスト
いよいよ完成間近です。パケットがhost2とhost3に届くことをテストします(図1の②)。最初のテストの保留マーク(pending)を消して、再び実行してみましょう。
失敗してしまいました。host2 がパケットを受信できていません。そういえば、flow_modしただけではパケットは送信されないので、明示的にpacket_outしてやらないといけないのでしたね(リスト14)。さっそく実行してみましょう。
すべてのテストに通りました! これでリピータハブとテストコード一式が完成です。
まとめ
Tremaのユニットテストフレームワークを使ってリピータハブを作りました。Tremaのsrc/examplesディレクトリの下にはテストコードのサンプルがいくつかありますので、本格的にテストコードを書く人は参考にしてください注1。今回学んだことは次の2つです。
- コントローラをユニットテストする方法を学びました。Trema はRuby のユニットテストフレームワークRSpecと統合されており、仮想スイッチのフローテーブルや仮想ホストの受信パケット数などについてのテストを書けます。
- テストをGiven、When、Thenの3ステップに分けて分析/設計する方法を学びました。それぞれのステップをRSpecのテストコードに置き換えることで、テストコードが完成します。
次回はTremaプロジェクト入門と題して、開発の舞台裏やメンバーの紹介、またTremaに付属するサンプルアプリを解説します。Tremaに加わりたい人や、参考になるソースコードを探している人に役立つ情報となる予定です。
最後に、OpenFlowプログラミングコンテストのお知らせです。InteropTokyo 2012において、ソフトウェアルータの実装コンテストである「オープンルータ・コンペティション」が開催されます。Tremaで書いたコントローラを試す良い機会ですので、腕に覚えのある方はぜひ参加してみてください!