SHOEISHA iD

※旧SEメンバーシップ会員の方は、同じ登録情報(メールアドレス&パスワード)でログインいただけます

CodeZine編集部では、現場で活躍するデベロッパーをスターにするためのカンファレンス「Developers Summit」や、エンジニアの生きざまをブーストするためのイベント「Developers Boost」など、さまざまなカンファレンスを企画・運営しています。

C#で始めるテスト駆動開発入門

TDDBC大阪の課題をC#でやってみる ~ クラス設計とTDD

C#で始めるテスト駆動開発入門(5)

  • X ポスト
  • このエントリーをはてなブックマークに追加

ステップ3 1: コーラが購入できるか判定する(続)

 商品ラックジュースを提供する()メソッドを実装したので、在庫を減らすことができるようになりました。ステップ3-1で後回しにしておいた、Is購入可能()メソッドに在庫切れの条件を追加することができます。

 コントローラーが在庫を知ることも必要になったので、商品ラッククラスとも連携しなければなりません。コントローラーのコンストラクターの引数を増やします。そのため、既存のテストコードにも修正が必要になります。

テストコード: 投入額は足りているが在庫不足で買えない
[TestCase]
public void Is購入可能Test_在庫不足で買えない() {
  コントローラー ctl = new コントローラー(new コインメック(), new 商品ラック());
  for (int i = 0; i < 5; i++)
    ctl._rack.ジュースを提供する(ジュース.コーラ);
  //Assert.IsFalse(ctl._rack.Is在庫有り(ジュース.コーラ));  // 確認

  ctl._coinMech.お金を投入する(100, 10, 10);
  Assert.AreEqual(false, ctl.Is購入可能(ジュース.コーラ));
}
製品コード: コントローラークラス
internal class コントローラー {

  internal コインメック _coinMech;
  internal 商品ラック _rack;  //←追加

  internal コントローラー(コインメック coinMech, 商品ラック rack){
    this._coinMech = coinMech;
    this._rack = rack;  //←追加
  }

  public bool Is購入可能(ジュース kind) {
    if (_coinMech.預り金 < kind.Price)
      return false;

    if (!_rack.Is在庫有り(kind))  //←追加
      return false;               //←追加

    return true;
  }
  //(後略)

 これでステップ3-1は完了です。ステップ3-2に戻ります。

ステップ3 2: コーラを購入する(続)

 このステップは、ジュースを提供して在庫を減らすところまで終わっていました。次は、預り金から代金を売り上げ金額に移す部分です。

売り上げ金額プロパティ

 売り上げ金額はコインメックに管理させましょう。まず、プロパティを作ります。

テストコード: 売り上げ金額プロパティ
[TestCase]
public void 売り上げ金額Test_最初は0円() {
  コインメック cm = new コインメック();
  Assert.AreEqual(0, cm.売り上げ金額);
}
製品コード: コインメッククラス
public int 売り上げ金額 { get; private set; }

 これはステップ3-4も満足することになります。

代金を貰う

 代金を貰う()メソッドで、預り金から代金を売り上げ金額に移します。

テストコード: 代金を貰う
[TestCase]
public void 代金を貰うTest() {
  コインメック cm = new コインメック();
  cm.お金を投入する(100, 10);

  cm.代金を貰う(100);
  Assert.AreEqual(10, cm.預り金);
  Assert.AreEqual(100, cm.売り上げ金額);
}
製品コード: コインメッククラス
public void 代金を貰う(int charge) {
  預り金 -= charge;
  売り上げ金額 += charge;
}

購入する

 これで、購入する()メソッドを組み立てることができます。仮実装→三角測量します。

 まず、買えない場合。これはステップ3-3に該当します。

テストコード: 購入する - 買えない場合
[TestCase]
public void 購入するTest_購入不可の時はnullが返る() {
  コントローラー ctl = new コントローラー(new コインメック(), new 商品ラック());
  // お金を投入していないから買えない
  Assert.IsNull(ctl.購入する(ジュース.コーラ));
}
製品コード: コントローラークラス
public ジュース 購入する(ジュース kind) {
  return null;
}

 続いて、買える場合。購入する()メソッドからコーラオブジェクトが返ってくるだけでなく、状態の預り金売上金額が変化します。

テストコード: 購入する - 買える場合
[TestCase]
public void 購入するTest_購入できる場合() {
  コントローラー ctl = new コントローラー(new コインメック(), new 商品ラック());
  ctl._coinMech.お金を投入する(100, 10, 10, 10); //130円入れたので、10円残るはず

  Assert.AreEqual(ジュース.コーラ, ctl.購入する(ジュース.コーラ));
  Assert.AreEqual(10, ctl._coinMech.預り金);
  Assert.AreEqual(120, ctl._coinMech.売り上げ金額);

  //Assert.AreEqual(10, ctl._coinMech.お金を払い戻す()); //確認(ステップ3-5)
}
製品コード: コントローラークラス
public ジュース 購入する(ジュース kind) {
  if(!Is購入可能(kind))
    return null;

  var product = _rack.ジュースを提供する(kind);
  _coinMech.代金を貰う(kind.Price);

  return product;
}

 最初からGREENになってしまうのでコメントアウトしましたが、お金を払い戻す()メソッドは預り金から代金を引いた額だけ出力しています。つまり、ステップ3-5もできています。

 これでステップ3は完成です。

コントローラーのクラス図

 ここまでのコントローラークラスは、次の図のようになっています。

コントローラーのクラス図(ステップ3終了時点)
コントローラーのクラス図(ステップ3終了時点)

 コントローラー自体は状態を直接には持っていません。メンバー変数にコインメック商品ラックを保持して、それらの連携を取ることが責務になっています。公開しているメソッドは、購入に関するものだけです。このクラスもまだまだシンプルで、これ以上の分割を考える必要性もなさそうです。

ステップ4 機能拡張

ステップ4 機能拡張


  1. ジュースを3種類管理できるようにする。
    • 在庫にレッドブル(値段:200円、名前"レッドブル")5本を追加する。
    • 在庫に水(値段:100円、名前"水")5本を追加する。
  2. 投入金額、在庫の点で購入可能なドリンクのリストを取得できる。

ステップ4 1: ジュースの種類を増やす

 1つめは、在庫に複数種類のジュースを持てるように、商品ラッククラスを拡張します。ただし、商品ごとにビンは1本だけ使うものとします。

レッドブルと水

 まず、レッドブルを用意しておかねばなりません。

テストコード: レッドブル
[TestCase]
public void レッドブルTest() {
  ジュース redbull = ジュース.レッドブル;
  Assert.AreEqual("レッドブル", redbull.Name);
  Assert.AreEqual(200, redbull.Price);
}

[TestCase]
public void 水Test() {
  ジュース water = ジュース.水;
  Assert.AreEqual("水", water.Name);
  Assert.AreEqual(100, water.Price);
}
製品コード: ジュースクラス
public static ジュース レッドブル {
  get { return new ジュース() { Name = "レッドブル", Price = 200, }; }
}
public static ジュース 水 {
  get { return new ジュース() { Name = "水", Price = 100, }; }
}

商品ラックにレッドブル用のビンを追加する

 商品ラッククラスのメンバー変数ビンを、コレクションに変更しなければなりません。その手順としては、ビン型をList<ビン>にいきなり変えてしまい、エラーになった部分を修正していく方法もあります。

 しかしここでは、一歩々々進めるやり方として、既存のメンバー変数はそのままに、新しくレッドブル専用のビンを追加してみます。ちょっと面倒な手順になりますが、REDになっている時間は短くてすみます。

テストコード: レッドブルの在庫を追加する
[TestCase]
public void 在庫を追加するTest() {
  商品ラック rack = new 商品ラック();
  for (int i = 0; i < 5; i++)
    rack.ジュースを提供する(ジュース.コーラ); //コーラの在庫を0にしておく
      
  rack.在庫を追加する(ジュース.レッドブル, 5);
  Assert.IsTrue(rack.Is在庫有り(ジュース.レッドブル));
}
製品コード: 商品ラッククラス
private ビン RedBullビン = new ビン() { 商品 = ジュース.レッドブル };  //暫定

public void 在庫を追加する(ジュース kind, int number) {
  RedBullビン.在庫数 += number;  //暫定
}

public bool Is在庫有り(ジュース kind) {
  if(kind == ジュース.レッドブル)  //暫定
    return RedBullビン.在庫数 > 0; 

  return ビン.在庫数 > 0;
}

 このRedBullビンメンバー変数は、次でList<ビン>型に変更します。

商品ラックにGetビン()メソッドを追加する

 ビンを選択するメソッドを作ることで、メンバー変数をコレクションに変えます。

テストコード: レッドブルを格納しているビンを取得する
[TestCase]
public void GetビンTest() {
  商品ラック rack = new 商品ラック();
  rack.在庫を追加する(ジュース.レッドブル, 5);

  Assert.AreEqual(ジュース.レッドブル, rack.Getビン(ジュース.レッドブル).商品);
  //Assert.AreEqual(5, rack.Getビン(ジュース.レッドブル).在庫数);
}
製品コード: 商品ラッククラス
//private ビン RedBullビン = new ビン() { 商品 = ジュース.レッドブル };  //暫定
private List<ビン> RedBullビン = new List<ビン>() { new ビン() { 商品 = ジュース.レッドブル } };  

public ビン Getビン(ジュース kind) {
  return RedBullビン.Find(b => (b.商品 == kind));
}

 ただし、RedBullビン.在庫数を参照している箇所がエラーになるので、修正します。

製品コード: 商品ラッククラス
public void 在庫を追加する(ジュース kind, int number) {
  //RedBullビン.在庫数 += number;
  RedBullビン[0].在庫数 += number;
}

public bool Is在庫有り(ジュース kind) {
  if(kind == ジュース.レッドブル)  //暫定
    //return RedBullビン.在庫数 > 0;
    return RedBullビン[0].在庫数 > 0; 

  return ビン.在庫数 > 0;
}

複数種類の在庫を追加する

 レッドブルの在庫を追加するようにしてみます。だんだんと、ちゃんとコレクションを使うコードになっていきます。ただし、まだ存在しないビンを要求された時は、自動的に生成するものとします。

テストコード: レッドブルの在庫を追加する
[TestCase]
public void 在庫を追加するTest_レッドブルと水() {
  商品ラック rack = new 商品ラック();
  rack.在庫を追加する(ジュース.レッドブル, 5);
  rack.在庫を追加する(ジュース.水, 3);

  Assert.AreEqual(5, rack.Getビン(ジュース.レッドブル).在庫数);
  Assert.AreEqual(3, rack.Getビン(ジュース.水).在庫数);
}
製品コード: 商品ラッククラス
public ビン Getビン(ジュース kind) {
  ビン bin = RedBullビン.Find(b => (b.商品 == kind));
  if (bin == null) {
    bin = new ビン() { 商品 = kind, };
    RedBullビン.Add(bin);
  }

  return bin;
}

public void 在庫を追加する(ジュース kind, int number) {
  //RedBullビン[0].在庫数 += number;
  Getビン(kind).在庫数 += number;
}

複数種類のジュースを提供する

 現状では、何か提供するといつでもコーラの在庫が減算されてしまいます。ジュースを提供する()メソッドとIs在庫有りプロパティを修正します。

テストコード: を提供するとの在庫だけが減る
[TestCase]
public void Is在庫有りTest_コーラと水() {
  商品ラック rack = new 商品ラック();
  rack.在庫を追加する(ジュース.水, 5);
  //Assert.IsTrue(rack.Is在庫有り(ジュース.コーラ)); //確認
  //Assert.IsTrue(rack.Is在庫有り(ジュース.水));     //確認

  for (int i = 0; i < 5; i++)
    rack.ジュースを提供する(ジュース.水); //水の在庫だけを0に

  Assert.IsTrue(rack.Is在庫有り(ジュース.コーラ));
  Assert.IsFalse(rack.Is在庫有り(ジュース.水));
}
製品コード: 商品ラッククラス
public bool Is在庫有り(ジュース kind) {
  //if(kind == ジュース.レッドブル)  //暫定
  //  //return RedBullビン.在庫数 > 0;
  //  return RedBullビン[0].在庫数 > 0; 
  //
  //return ビン.在庫数 > 0;
  return Getビン(kind).在庫数 > 0;
}

public ジュース ジュースを提供する(ジュース kind) {
  ビン.在庫数--; //後で消す
  Getビン(kind).在庫数--;
  return kind;
}

商品ラッククラスからメンバー変数ビンを取り除く

 商品ラッククラスのメンバー変数ビンは、製品コードでは使わなくなったので削除しましょう。

製品コード: 商品ラッククラス
//public ビン ビン{ get; private set; }

public ジュース ジュースを提供する(ジュース kind) {
  //ビン.在庫数--; //後で消す
  Getビン(kind).在庫数--;
  return kind;
}

 商品ラッククラスのメンバー変数ビンをテストしているところを探して、Getビン(ジュース.コーラ)に置き換えてもGREENのままであることを確かめます。そうしたら、 メンバー変数ビンを削除します。

テストコード: ビンGetビン(ジュース.コーラ)に置き換え
[TestCase]
public void ConstructorTest_初期状態でコーラが5本() {
  商品ラック rack = new 商品ラック();

  Assert.AreEqual(5, rack.Getビン(ジュース.コーラ).在庫数);  //←
  Assert.AreEqual("コーラ", rack.Getビン(ジュース.コーラ).商品.Name);  //←
}

[TestCase]
public void ジュースを提供するTest_コーラ() {
  商品ラック rack = new 商品ラック();

  Assert.AreEqual(ジュース.コーラ, rack.ジュースを提供する(ジュース.コーラ));
  Assert.AreEqual(4, rack.Getビン(ジュース.コーラ).在庫数);  //←
}

商品ラッククラスのリファクタリング

 メンバー変数をコレクションに変更できたものの、RedBullビンという名前はいただけません。名前をビンリストに変え、そのほかも少々リファクタリングしておきましょう。

製品コード: 商品ラッククラス(リファクタリング後)
internal class 商品ラック {

  private List<ビン> ビンリスト = new List<ビン>();

  public ビン Getビン(ジュース kind) {
    ビン bin = ビンリスト.Find(b => (b.商品 == kind));
    if (bin == null) {
      bin = new ビン() { 商品 = kind, };
      ビンリスト.Add(bin);
    }
    return bin;
  }


  public 商品ラック() {
    ビンリスト.Add(new ビン() { 商品 = ジュース.コーラ, 在庫数 = 5, });
  }

  public void 在庫を追加する(ジュース kind, int number) {
    Getビン(kind).在庫数 += number;
  }

  public bool Is在庫有り(ジュース kind) {
    return Getビン(kind).在庫数 > 0;
  }

  // TODO: int Get在庫数(ジュース)

  public ジュース ジュースを提供する(ジュース kind) {
    Getビン(kind).在庫数--;
    return kind;
  }
}

 このように細かいステップを踏んで、メンバー変数の型をコレクションに直してきました。REDの時間が短くてすむとは言うものの、けっこう面倒ですし、途中で間違えてしまいそうですね。コレクションにしなければならないと分かっているときには、最初からコレクションにしてしまいましょう。

商品ラックのクラス図

 ここまでで商品ラッククラスは、このようになっています。

商品ラックのクラス図(ステップ4-1終了時点)
商品ラックのクラス図(ステップ4-1終了時点)

 商品ラックビンのコレクションを持っており、そこにジュースの種類と在庫数を保持しています。在庫を管理し、提供することが責務になっています。公開しているメソッドのうち、Getビン()は責務からちょっと外れていますし、他のオブジェクト(コインメックコントローラー)からも必要とされていません。テストのためだけに公開していますが、ちょっと嫌な感じです。Get在庫数(ジュース)といったメソッドを作れば、Getビン()メソッドをprivateに変えられるでしょう。

ステップ4 2: 購入可能なジュースのリスト

 このステップでは、投入金額で買えるジュースで在庫があるものだけのリストを取得できるようにします。まず在庫のある商品のリストを作り、その中から預り金以下の商品だけを抜き出せばよいですね。

商品ラックは在庫を持っているジュースを答えられる

 まず、商品ラッククラスに、在庫しているジュースのリストを返す在庫ジュースプロパティを作ります。

テストコード: 在庫しているジュースのリスト
[TestCase]
public void 在庫ジュースTest() {
  商品ラック rack = new 商品ラック(); //初期状態でコーラは在庫している
  rack.在庫を追加する(ジュース.レッドブル, 1);
  rack.ジュースを提供する(ジュース.レッドブル); //レッドブルの在庫は0
  rack.在庫を追加する(ジュース.水, 3);
  CollectionAssert.AreEqual(
    new List<ジュース>() { ジュース.コーラ, ジュース.水 }, 
    rack.在庫ジュース
  );
}
製品コード: 商品ラッククラス
internal IEnumerable<ジュース> 在庫ジュース {
  get {
    return ビンリスト.Where(bin => (bin.在庫数 > 0)).Select(bin => bin.商品);
  }
}

コントローラーが購入可能な商品リストを答える

 コントローラーコインメックの預り金も知っていますから、購入可能な商品を答えることができます。

テストコード: 購入可能な商品リストを得る
[TestCase]
public void Get購入可能リストTest() {
  コントローラー ctl = new コントローラー(new コインメック(), new 商品ラック());
  ctl._rack.在庫を追加する(ジュース.レッドブル, 5);
  ctl._rack.在庫を追加する(ジュース.水, 5);
  // これで、コーラ・レッドブル・水のどれも在庫が5

  ctl._coinMech.お金を投入する(100, 10, 10); 
  //120円入れたので、コーラと水だけが買える

  CollectionAssert.AreEquivalent(
    new List<ジュース>() { ジュース.コーラ, ジュース.水, }, 
    ctl.Get購入可能リスト()
  );
}
製品コード: コントローラークラス
public IEnumerable<ジュース> Get購入可能リスト() {
  return _rack.在庫ジュース.Where(kind => (kind.Price <= _coinMech.預り金));
}

次のページ
ステップ5 釣り銭と売り上げ管理

この記事は参考になりましたか?

  • X ポスト
  • このエントリーをはてなブックマークに追加
C#で始めるテスト駆動開発入門連載記事一覧

もっと読む

この記事の著者

biac(ばいあっく)

HONDA R&Dで自動車の設計をやっていた機械屋さんが、技術の進化スピードに魅かれてプログラマーに。以来30年ほど、より良いコードをどうやったら作れるか、模索の人生。わんくま同盟の勉強会(名古屋)で、よく喋ってたりする。2014/10~2019/6 Microsoft MVP (Windows Devel...

※プロフィールは、執筆時点、または直近の記事の寄稿時点での内容です

この記事は参考になりましたか?

この記事をシェア

  • X ポスト
  • このエントリーをはてなブックマークに追加
CodeZine(コードジン)
https://codezine.jp/article/detail/6633 2012/06/29 14:00

おすすめ

アクセスランキング

アクセスランキング

イベント

CodeZine編集部では、現場で活躍するデベロッパーをスターにするためのカンファレンス「Developers Summit」や、エンジニアの生きざまをブーストするためのイベント「Developers Boost」など、さまざまなカンファレンスを企画・運営しています。

新規会員登録無料のご案内

  • ・全ての過去記事が閲覧できます
  • ・会員限定メルマガを受信できます

メールバックナンバー

アクセスランキング

アクセスランキング