Skip to content

Instantly share code, notes, and snippets.

@sile
Last active January 28, 2016 05:36
Show Gist options
  • Save sile/5124984 to your computer and use it in GitHub Desktop.
Save sile/5124984 to your computer and use it in GitHub Desktop.
実践テスト駆動開発第二回

第三部 動くサンプル

ある程度大きな規模の実際に動くサンプルを呈示する

  • 現実のプロジェクトの即した複雑性
    • 外部コンポーネントを含む (swing, XMPP)
    • イベントドリブン、マルチスレッド、分散環境
  • 現実味のあるストーリー
    • 「下した決定が間違っていたことが後になって判明したせいで引き返さねばならなくなった」
    • 往々にして発生するが、放置しておくと、後になって高い代償を払うことになる
  • きわめてインクリメンタルな開発
    • テストによって保護された継続的な(小さく安全な)リファクタリング
    • 常に動くシステム

第九章 オークションスナイパーを作動させる

概要:

  • 開発に入るまでのストーリー
  • 実装するアプリケーションの概要や要件、用語の定義等を行う
  • とっかかりとなるフィーチャ一覧もここで呈示

[1] 最初から始める

オークションの競り落としを自動で行うアプリケーションの構築を依頼された。

  • オークションスナイパー を作成することに決定
    • オンラインオークションを監視し、逆指値に達するまでは、自動で入札を行う
    • バイヤー集団(依頼者)は、何を構築すべきかを明確にする上で、手を貸してくれることに合意

混乱を避けるために、まず用語定義(合意)が必要になった。

  • 商品: 一意に定め、購入することができるもの
  • 入札者: 商品を購入したいと思っている人または組織
  • 入札: 入札者が商品に対しての所定の代金を払うという声明
  • 現在価格: 商品に対して行われた、現在もっとも高額な入札
  • 逆指値: 入札者が商品に対して払える最高金額
  • オークション: 商品に対する入札を管理するプロセス
  • オークションハウス: オークションを開催する団体

要件をリストアップしたが、一度に全てを作るのは無理そうだった。

  • まずは、基本的な機能を備えたアプリケーションを作ということで合意
  • 一旦、そのようなアプリケーションができあがれば、後からより良いものにしていくことも可能

調査の結果、オークションのオンラインシステムに関して、以下のことが判明

  • 競売は商品ごとに行われる => 特定の競売は商品IDに紐付けて参照することに
  • スナイパーアプリケーションは購入した商品の管理はする必要がない => 他のシステムが担当

オークションスナイパーの要件

  • Java Swingアプリケーション
  • ユーザは一度に複数の商品を入札可能
  • 対象の各商品に対して、IDと逆指値、現在価格、ステータスを表示
  • バイヤーはユーザーインターフェースを通じて、対象商品を追加可能
  • オークションハウスから来るイベントに応じて、表示内容は変化
  • ラフバージョンのイメージ: 図9-1

サザベー(オークションハウス)のオンラインシステム

  • やりとりにはXMPP(jabber)を使用
  • 図9-2: オンラインシステム上での、バイヤーとオークションハウスの関係
    • 競売の進行に合わせて、コマンドとイベントを送りあう

[2] オークションと通信する

オークションプロトコル

コマンドとイベント:

  • 入札者は コマンド を送信する

    • 参加コマンド: オークション参加時に送信。XMPPメッセージの送り手=入札者。チャットセッションの名称で商品を特定。
    • 入札コマンド: 入札を行う時に価格を送信。
  • オークションは イベント を送信する

    • 価格イベント: 現在受理されている価格(+ 入札者および次の最低増額分)を報告するイベント
      • 新しい入札者が参加した場合、その入札者に送信
      • 新しい入札が受理された場合、全ての入札者に送信
    • 終了イベント: オークション終了を報告するイベント

図9-3: 入札者の振る舞いのステートマシン

  • 参加 => 入札(ループ) => 終了 => 落札 or 落札失敗
  • 第18章まで逆指値は無視

XMPPメッセージ

  • 各メッセージ(イベント or コマンド)は、単一行で表現され、 "{キー}: {値};" の繰り返し (本書の例を参照)
  • 対象商品はログイン名で識別
    • ID12793の商品を入札したいなら「オークション-12793」というユーザとチャットを開始する

[3] 安全に作る

このように小さなアプリケーションでさえ、一度に作るには大きすぎる。

  • 完成までに歩むことになるであろうステップを、おおまかに理解する必要がある
  • インクリメンタルな開発では、機能をスライスし、一度に少しずつ構築できるようにすることが重要
    • スライスは、
      • いつ完成したかをチームがわかるように、意味を持ち、具体的でなければならない
      • ひとつの概念に集中し、すぐに完成できるよう十分小さいものでなければならない
    • メリット:
      • 一貫した小さな塊に分割することで、開発のリスクが管理可能になる
      • 進捗に対する具体的なフィードバックを定期的に得られるので、ドメインや扱う技術についてより多くのことをチームが理解したときに、計画を調整することが可能

さしあたりやるべきことは、

  • スナイパーアプリケーションのためのインクリメンタルな開発ステップを一通り明らかにする
  • 最初に作るべきものは構築できる最小のフィーチャ => 「動くスケルトン」(P.36)
    • Swing,XMPPおよびアプリケーションを最短距離で通り抜けるスケルトン
    • 各コンポーネントをつなぎ合わせられることを示せれば十分
    • 後に続くステップはそれぞれ、既にあるアプリケーションに対して複雑さを一要素ずつつけ加える

結果として出てきたフィーチャ一覧:

  1. 商品ひとつ: 参加し、入札せずに落札に失敗する
  • コアとなる基盤をまとめる立ち上げ用。第10章で扱う
  1. 商品ひとつ: 参加し、入札し、落札に失敗する
  • 基本的な疎通に入札を追加する
  1. 商品ひとつ: 参加し、入札し、落札する
  • 落札者を識別する。ユーザーインターフェースに価格の詳細表示を追加する
  1. 複数の商品を扱う
  2. ユーザインタフェースから商品を追加する
  3. 逆指値で入札をやめる

ユーザインターフェースの優先度は逆指値よりも高く設定。

より複雑なシナリオは、上記のフィーチャ一覧の実装が終わってから。

[4] 現実はこうはいかない

  • これは、現実的なアーキテクチャではない
  • これは、アジャイルな計画づくりではない
  • これは、現実的なユーザビリティ設計ではない

第十章 動くスケルトン

概要:

  • 開発環境のセットアップ
  • 最初のエンドツーエンドテストを記述

[1] スケルトンの全貌を明かにする

まずは「動くスケルトン」の構築から始める。

  • 要件の十分な理解や大まかなシステム構成の提案および検証をできるようにする
  • ほとんどのプロジェクトでは動くスケルトンの開発には驚くほどの労力がかかる
    • アプリケーションとその位置づけに関するあらゆる種類の疑問を一掃しなければならない
    • 擬似本番環境に対するビルド、パッケージング、デプロイを自動化するには、あらゆる種類の技術的、組織的疑問を一掃しなければならない
  • 動くスケルトンの作成もテストを書くことから始める

[2] 一番最初のテスト

動くスケルトンは、システムの全てのコンポーネントを網羅する必要がある

  • UI、スナイパーコンポーネント、オークションサーバーとの通信、etc
  • テスト内容は最小限で構わない
    • フィーチャリストの一番上の項目
    • 機能としては、オークションへの参加、終了のみ
    • 各コンポーネントが通信可能で、システムを外部からテストできるかどうかが確認できれば十分

テストは、

  • あたかも実装されているかのようにテストを書き始め、その後で、それが動くために必要なものを全て埋め込んでいく
    • 「希望的観測によるプログラミング」
  • システムに 何を させたいのかに集中でき、それを どう 動かすかに関する複雑さにとらわれずにすむ
  • 自分たちの意図をプログラミング言語の表現力の範囲で、できる限り明確に説明するようにテストを書き上げる
    • その上で、システムをテストする方式をサポートするような基盤を構築する
    • 既存の基盤に合うようにテストを書くのではない
    • 準備しなければならないものが非常に多いので、労力は多
    • 基盤が整えば、フィーチャを実装してテストを通るようにすることができる

動くスケルトンで求めるテストシナリオ:

  1. オークションに商品が出品されていて、
  2. オークションスナイパーがそのオークションに入札を始めたら、
  3. オークションは、オークションスナイパーからの「参加」リクエストを受信する。
  4. オークションが「終了」を宣言したら、
  5. オークションスナイパーは、落札に失敗したことを表示する。

上のシナリオを、実際に動くテストに変換する必要がある。

  • テストフレームワークにはJUnitを使う
  • オークションシステムには スタブ を使用する
    • サザベーのオンラインテストサービスは無料で使うことができないので、次善の策
  • スナイパーアプリケーションは、実際のUIを通して制御し、テストする
    • UI制御部分は切り離して、スナイパーとオークションの関係という観点から、テストを記述したい
    • Swingを扱うコードは、ApplicationRunnerクラスに隠蔽する

最初のテストコード (p.90)

命名規則:

  • テストを実行するためのイベントのトリガーになるメソッドの名前は命令調 (ex. startBiddingIn)
  • 期待する状態をチェック(アサート)するメソッドの名前は説明的 (ex. showsSniperHasLostAuction)

[3] いくつかの最初の選択

テストを通るようにするために、次の四つのコンポーネントを用意する必要がある:

  • XMPPメッセージブローカー
  • XMPP上で通信できるスタブオークション
  • GUIテストフレームワーク
  • マルチスレッドの非同期アーキテクチャを扱うことができるテストハーネス

プロジェクトを、ビルド/デプロイ/テストが自動化されたバージョンコントロールの元に置く必要もある。

ユニットテストに比べてやることは多いが、欠かすことができない。

  • テストを書くという行為がシステムの開発を推進。
  • 最初のエンドツーエンドテストをやり切ることで、パッケージングやデプロイといった、避けては通れない構造上の意思決定をいくつかやらなければならなくなる。

まずは、パッケージの選択:

  • XMPPメッセージブローカー: Openfire(サーバー)、Smack(クライアント)
  • テストフレームワーク: WindowLicker
  • 基盤の構成: 図10-2

エンドツーエンドテスト

  • スナイパーのエンドツーエンドテストは、非同期処理を扱う必要がある
    • ユニットテストとは異なる
    • 今回は WindowLicker で、Swingおよびメッセージング基盤の非同期処理を自然にカバーすることができる
  • UIやログ等の外部状態から、結果を知る必要がある
    • 一般には、制限時間付きのポーリングが使われる
    • 外部状態は、検出可能な程度、存続していなければならない
      • アプリケーションを制御し、段階を踏んでシナリオを進めるというテクニックが良く使われる
  • 時間がかかり、壊れやすくもなる
    • 「その日、たまたま失敗した」ということが起こり得る

準備完了

この最初のテストは、本当の意味でのエンドツーエンドテストではない。

  • 本物のオークションサービスを使っていない
  • TDDでは、何をテストし、最終的にどうやってすべてを網羅するかという境界を設定するスキルが重要
  • 偽者を使っていることは、既知のリスクとして記録しておく
    • 意味のあるトランザクションを実現するのに十分な機能が作れたら、できるだけ早く本物のサーバに対してテストを行う

さて、そろそろ作り始めた方がよさそうだ。

第十一章 最初のテストを通す

概要:

  • 「動くスケルトン」が通るまでの流れ
  • テスト失敗 => 実装、の流れを小さなスライス単位で繰り返していく
  • 「とりあえず」とか「後で」が多い

[1] テスト装置を構築する

  • テスト実行前に、Openfireサーバの起動と各種アカウントの作成、を行っておく
  • テストが実行されると、アプリケーションのインスタンスとニセのオークションが開始され、相互に通信を行う
    • テスト基盤として ApplicationRunner と FakeAuctionServer の二つがコンポーネントが必要になる

アプリケーションランナー

ApplicationRunnerクラス

  • Swingアプリケーションの管理と通信をすべてラップするオブジェクト
    • コマンドラインから起動されたかのようにアプリケーションを実行する
    • メインウィンドウの更新や状態問い合わせ、アプリケーションのシャットダウンなどを行う
    • ほとんど面倒な仕事は WindowLicker が肩代わりしてくれる
  • WindowLickerの ComponentDriver を使ってテストする
    • Swingユーザインターフェースの機能を操作するオブジェクト
    • 操作対象Swingコンポーネントが見つけることができなければ、タイムアウトを起こしてエラーになる
    • ここでのテストでは、所定の文字列を表示しているラベルコンポーネントを探す

ApplicationRunnerのコード (p.94)

  1. 新規スレッドでのアプリケーションの起動 (main呼び出し)
  • 本当は別プロセスにするのが理想
  1. シンプルにするために、入札商品は一つだけと想定
  2. 例外が発生した場合は、スタックトレースを表示
  3. フレームやコンポーネントを探す際のタイムアウト時間は 1秒に設定
  4. UIの状態が「参加中」に変わるのを待つ(アサート)
  5. スナイパーがオークションで落札失敗時に、UIの状態が「落札失敗」に変わるのを待つ(アサート)
  6. テスト終了後に、ウィンドウの後始末を行う

AuctionSniperDriverのコード (p.95)

  • ApplicationRunnerの中で使われ、Swingに関する操作を担当
  • WindowLicker の JFrameDriver を簡単に拡張したクラス
  • showsSniperStatus() は、スナイパーの状態を表示するラベルを検索し、ステータスを確認する

ニセのオークション

FakeAuctionServerクラス

  • 本物のオークションシステムを模倣した、テスト用の代替サーバー
  • 責務は三つ
    • XMPPブローカーに接続して、チャットに参加するというスナイパーからのリクエストを受理する
    • スナイパーからのチャットメッセージを受信する or 一定のタイムアウト時間内にメッセージが到着しなかった場合に失敗する
    • サザベーオンラインが定義した通りのメッセージをスナイパーに送り返す
  • Smack(XMPPクライアントライブラリ)はイベント駆動
    • リスナオブジェクトを登録
    • 発生するイベントは二種類
      • チャットに関するイベント (ex. 人が参加する)
      • チャットの中で起こるイベント (ex. メッセージが受信された)

FakeAuctionServerのコード

  • 前半: startSellingItem() を途中まで実装 (p.96)

    1. XMPPブローカーに接続
      • ログイン名は商品ID
    2. チャットマネージャに ChatManagerListener を登録
      • Smackはこのリスナを Chatオブジェクト を引数に渡して呼び出す
      • Chatオブジェクトはチャットのセッションを表現
    3. スナイパーとのメッセージやりとり用に Chatオブジェクト を保持
  • 後半 (p.97)

    • スナイパーからのメッセージを受理するために MessageListener を チャット に追加する
    • メッセージ受信待機用に BlockingQueue をラップした SingleMessageListenerクラス を用意する
      • ラッパーを用意するのは、意図を明確にするため
    • 処理
      1. スナイパーの「参加」知るために なんらかの メッセージの到着を待つ(アサート)
      2. オークションの終了を通知するために 空の メッセージを送信する
      3. XMPP接続の切断
      4. タイムアウト時間内に、何らかのメッセージを受信したかどうかをアサート。Hamcrestのマッチャー構文。

メッセージブローカー

Openfireのインスタンスをローカルホストでセットアップしておく。

[2] テストを失敗させ、そして、通るようにする

ここまでで、(失敗する)テストを実行させるための基盤は整った。

この後は、小さなスライスを順次追加していき、最終的にはテストが通るようにする。

  • このテクニックを使い始めたときは、あまりに手間がかかりすぎると感じた
    • 「コードを書くだけでいいじゃないか、何をすべきかわかっているのだから!」
  • しかし、時間が経つにつれて、このテクニックで前より時間がかかることはなく、進捗がはるかに予想しやすくなることに気がついた
    • 一度にひとつの側面に集中することで、確実にそれを理解できるようになる
    • 何かを動くようにすれば、それを動かし続けることはできるもの
    • 実装するよりも、説明する方に時間がかかるのだ (?)

ステップ1: 最初のユーザーインターフェイス

■テストの失敗

スナイパー側# 「Auction Sniper Main」という名前のUIコンポーネントが見つからない。 (p.100)

■実装

ウィンドウ(MainWindowクラス)の追加(p.101)

■メモ
  • 図11-2
  • 最小限だが、アプリケーションウィンドウの開始とそこへの接続が確認できる
  • 一歩前進し、自分たちのテストハーネスがうまく機能することが分かる

ステップ2: スナイパーの状態を表示する

■テストの失敗

スナイパー側# ウィンドウに現在の状態(Joining)が表示されていない (p.102)

■実装

スナイパーの状態を表示するラベルを MainWindow に追加 (p.102)

■メモ
  • 図11-3
  • アプリケーションの内容が表示できるようになった

ステップ3: オークションに接続する

■テストの失敗

オークション側# スナイパーからの「参加」リクエストを受信していない (p.103)

■実装
  • **Main.main()**内に チャットへの接続 と 空メッセージの送信 を追加
  • チャットには 空のMessegeListener を登録 (まだスナイパーのメッセージ受信は関心外)
■メモ
  • スナイパーからオークションへの接続を確立が可能になった
    • コマンドライン引数の解析 と Smackライブラリの使用 を解決
    • まだ送信メッセージの中身は空 (まだ不要。必要最小限で)

この実装は素朴過ぎる?

  • 醜いコードを すこし 書き、問題がどう解消されるかを確認することは価値がある
    • 先に進みすぎる前に考え方をテストすることは役に立つし、時には結果に驚かされることもあるかもしれない
    • 重要なのは、絶対に醜いまま放置しないこと

あえて接続のためのコードをMainWindowを生成する云々(TODO)

ステップ4: オークションのレスポンスを受信する

■テストの失敗

スナイパー側# オークションからの「落札失敗(Lost)」レスポンスの受信および表示ができていない (p.105)

■実装
  • スナイパーのチャットへの接続/通信部分を joinAuction() に分離
  • オークションからのメッセージを待機し、何らかの メッセージを受信したら UI に「落札失敗」ステータスを表示
    • 以前は空だった MessageListener に上記処理を追加
  • notToBeGCd
  • これで全てのテストが通るようになった
■メモ
  • 図11-4
  • スナイパーがオークションとの接続を確立し、レスポンスを受け取って結果を表示したことが確認できている

[3] 必要最小限

ここまでで動くスケルトンの構築が完了。

  • 最初の動くスケルトンを組み立てるにはかなりの集中が必要
  • 重要なのは、エンドツーエンドシステムの最初の構造を設計し検証すること
    • エンドツーエンド には動作環境へのデプロイも含まれる
    • 自分たちの行ったパッケージングやライブラリ、ツールの選択が実際にうまくいくことを証明する
  • ここでは詳細なコードの設計に関しては、それほど労力をかけていない
    • 例えばスナイパーのメッセージには一切中身が入っていない
      • 中身を入れてしまうと、通信とイベント処理がうまくいくことの確認から逸れてしまう
    • 単に下地を作っただけ。設計の苦労はすぐに味わうことになる。

この章ではハイライトだけを拾い上げて紹介している。

  • ライブラリの選択までのあれこれ、上手い使用方法の調査
  • プロジェクトの目的に関する議論

更新されたTODOリスト: 図11-5

次のステップでは、実際の機能の構築を始める。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment