ソフトウェア工学2025の中間レポート本文. Pythonで自販機の簡単なモデルを作成した.
プログラム本体「vendingMachine.py」とテストケース「test_vendingMachine.py」の2つから構成されている. TDDの正典に記載のステップを踏襲して開発を進めた.
vendingMachine.pyがカバーするべき項目を列挙する.
- 初期状態では
- 自販機内のお金は0円である.
- 初期状態では飲み物の在庫は充填されている.(満タンの本数を5本とする.)
- ユーザーがお金を入れると自販機内のお金が増える.
- ユーザーが飲み物のボタンを押すと,「飲み物の金額 <= 自販機内のお金」の場合,
- ユーザーは押した飲み物が入手できる.
- 押した飲み物の在庫のみ1減る.
- 自販機内のお金が減る.
- ユーザーが飲み物のボタンを押すと,「飲み物の金額 > 自販機内のお金」の場合,
- ユーザーは何も入手しない.
- 飲み物の在庫は変化しない.
- 自販機内のお金は変化しない.
- ユーザーが在庫不足の飲み物のボタンを押すと,
- ユーザーは何も入手しない.
- 飲み物の在庫は変化しない.
- 自販機内のお金は変化しない.
- ユーザーがお釣りのボタンを押すと,
- ユーザーはお釣りを入手する.
- 自販機内のお金が0円に戻る.
ステップ1で列挙したテスト項目を1つずつpythonコードに変換していく. assertを用いて,各状態において関数を実行したときに事後条件が成立するかを調べていく.
ステップ2で設定したテストを通過できるように,メインのvendingMachine.pyを実装していく. 今回の例においては「ボタンを押す」「入金する」といったユーザーの動作を自販機クラスのメソッドとして実装している.
テストケースを追加していく過程で,既存の内部構造ではやりずらいと感じた時に,仕様を変えない範囲で改善している.
例えば,自販機クラスで,飲み物を管理するdrinksプロパティの実装.
最初の実装では,在庫のフィールドのみ存在していれば良かったので,
drinks = { 商品名: 在庫数 }
と一次元の辞書で管理していたが,価格のフィールドを追加するにあたって
drinks = { 商品名: { "stock": 在庫数, "price": 価格 } }
というように構造を変更する必要が発生する.
その他のリファクタリングの例としては以下のようなものも体験した.
- メソッドの分割
飲み物を買えるか否かの判定を行う関数をcan_buy()を追加することによって見やすく - メソッド名,変数名の変更
def buy_drinks(self, drink)→def buy(self, drink_name)として,メソッド・引数の意味を明確に
最初の開発目標はステップ1で挙げた6つのテストをクリアすることなので,それまでステップ2-4を繰り返す.
システムとして形が完成した後も,新たなリリースがある時は再びステップ2-4による開発を繰り返す.
今回の例では,以下の2つのリリースを行った.
- 新商品「コーヒー」の追加
- 飲み物の購入成立時に自動でお釣りを返却するように機能を変更
(それまではお釣りボタンを押さないと自販機内にお金が残ったまま)
このような追加や変更は外部から見た振る舞いの変化なので,単なるリファクタリングではなく仕様の変更と言える. 1のように元々あるフィールドに種類を追加するだけなら,その部分のテストケースを追加すれば良い.(コーヒー購入のテストだけ) しかし2のような大幅な仕様の変更では,それ以前に作成したテストケースが壊れる可能性がある. したがって,以前に作成したテストケース(主に飲み物の購入成立時の動作)に対して,削除または期待する振る舞いに修正する必要がある. TDDでは,一度成立したテストは不変なわけではなくその時の仕様にしたがっているだけで,仕様の変更が発生すれば自然とテストの修正も全体に発生するはずである.個人的な考察としては,ここで仕様変更があったときにテストケースを大量に変更しなければならないようなことが起こったら,それはプログラムの設計的に各動作の責任が分離できていないのが原因ではないかと思う.よってTDDは設計上の問題を発見するという側面もあるのかもしれないと捉えた.