SHOEISHA iD

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

DeveloperZine(デベロッパージン)- エンジニアの意思決定を支える技術情報メディア ProductZine

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

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

TDDでデータベースと付き合う方法

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

ダウンロード CsTdd07.zip (474.5 KB)

DbCommandの取扱いも分離する

 前よりはだいぶ見通しが良くなりました。それでも、usingの入れ子が三重にもなっているのは鬱陶しいものです。ロジックの中でDbCommandを扱っていますが、それに意味はあるのでしょうか? そこもDbCommandHelperに移してしまえばよさそうです。テストコードと改良したDbCommandHelperは掲載しませんが、検索するメソッドは次のようにリファクタリングできます。詳細はサンプルコードをご覧ください。

製品コード: [改3] DbCommandも使わなくなった前方一致で姓名を検索する()メソッド
public static IList<Customer> 前方一致で姓名を検索する(string head) {
  var list = new List<Customer>();
  if (string.IsNullOrWhiteSpace(head))
    return list;

  using (DbCommandHelper改3 helper = new DbCommandHelper改3()) {

    var cmdText =
@"SELECT  Country, [Company Name], [Customer ID], [Contact Name]
FROM      Customers
WHERE     ([Contact Name] LIKE @p1) OR ([Contact Name] LIKE @p2)";
    var param1 = head + "%";
    var param2 = "% " + head + "%";

    using (DbDataReader reader = helper.ExecuteReader(cmdText, param1, param2)) {
      while (reader.Read()) {
        var customer = new Customer() {
          Country = reader.GetString(0),
          CompanyName = reader.GetString(1),
          CustomerID = reader.GetString(2),
          ContactName = reader.GetString(3),
        };
        list.Add(customer);
      }
    }
  }
  return list;
}

DbDataReaderをモックで置き換えるのは面倒

 上のコードでDbDataReaderを受け取って何をしているかというと、whileループで検索結果をCustomerオブジェクトに詰め替えているだけです。DbDataReaderをモックやダミー実装に置き換えるのは、とても面倒なので、このままではユニットテストすべてで実際のデータベースへのアクセスが必要になってしまいそうです。

 DbCommandHelperには、DbDataReaderではなく、IEnumerable<Customer>を返してもらうようにしておきましょう。モックやダミーに置き換えるのが、とても楽になります。テストコードは省略しますので、詳しくはサンプルコードをご覧ください。

製品コード: [改4] IEnumerable<Customer>を返すようにしたDbCommandHelperクラス
public class DbCommandHelper改4 : IDisposable {

  private SqlCeConnection _connection;
  internal SqlCeConnection Connection {
    get {
      // 【省略】
    }
  }

  internal SqlCeCommand Command { get; private set; }

  internal DbCommand CreateCommand(string commandText) {
    this.Command = new SqlCeCommand(commandText, Connection);
    return this.Command;
  }

  internal void AddStringParameter(DbCommand cmd, string paramName, string parameter) {
      // 【省略】
  }

  internal DbDataReader ExecuteReader(string cmdText, string param1, string param2) {
    this.CreateCommand(cmdText);
    this.AddStringParameter(this.Command, "@p1", param1);
    this.AddStringParameter(this.Command, "@p2", param2);
    return this.Command.ExecuteReader();
  }

  public IEnumerable<Customer> FindCustomers(string cmdText, string param1, string param2) {
    var results = new List<Customer>();

    using (DbDataReader reader = this.ExecuteReader(cmdText, param1, param2)) {
      while (reader.Read()) {
        var customer = new Customer() {
          Country = reader.GetString(0),
          CompanyName = reader.GetString(1),
          CustomerID = reader.GetString(2),
          ContactName = reader.GetString(3),
        };
        results.Add(customer);
      }
    }
    return results;
  }


  public void Dispose() {
      // 【省略】
  }
}

 ご覧のとおり、DbCommandHelperクラスがずいぶん肥大化してしまい、また、名前もふさわしくなくなってきたので、こちらもそろそろリファクタリングが必要です(本稿ではここまでとします)。

 製品コードは、リファクタリングして次のようになります。

製品コード: [改4] DbDataReaderも使わなくなった前方一致で姓名を検索する()メソッド
public static IList<Customer> 前方一致で姓名を検索する(string head) {

  if (string.IsNullOrWhiteSpace(head))
    return new List<Customer>();

  using (DbCommandHelper改4 helper = new DbCommandHelper改4()) {

    var cmdText =
@"SELECT  Country, [Company Name], [Customer ID], [Contact Name]
FROM      Customers
WHERE     ([Contact Name] LIKE @p1) OR ([Contact Name] LIKE @p2)";
    var param1 = head + "%";
    var param2 = "% " + head + "%";

    return helper.FindCustomers(cmdText, param1, param2).ToList<Customer>();
    //※ using System.Linq; が必要
    //※ なお、LINQ を有効に使うには、ここでは ToList<Customer>() せずに返した方が良い。
  }
}

 これで、最初に挙げた問題点の2つめ「ロジックは、できるだけ環境に依存しないコードにしたい」は、だいたい解消できたでしょう。筆者の主観としては、まだロジックがSQL Serverに依存していることが気になります。SQL文を扱う部分も切り出して、ロジック→SQL文ハンドリング→データベースアクセスという三層構造にしたいと思うところですが、そこはケースバイケースだと思います。

 このあと、FindCustomers()メソッドを持つインターフェースを定義して、DIコンテナを使うなどして実装をすげかえられるようにするのは、難しくないでしょう。3つめの問題点「モックやダミーに置き換えるのが困難」も、解消できています。

モックやダミーに置き換えやすい構造にする

 今回のサンプルで示したビジネスロジックは検索を1回やっているだけですが、通常はビジネスロジックの一連の流れの中で何回もデータベースへのアクセスがあるでしょう。それぞれのアクセスを、個別にモックやダミーに置き換えられるように作っていくと、ロジックのTDDがはかどります。

 この考え方は、データベースへのアクセス方法がなんであろうと、変わりません。LINQ to SQLだろうとEntity Frameworkだろうと、ビジネスロジックの処理の流れからデータベースへのアクセス部分を切り離せるように作っていくとよいです。

次のページ
データベースの内容を安定させる

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

C#で始めるテスト駆動開発入門連載記事一覧

もっと読む

この記事の著者

biac(ばいあっく)

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

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

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

この記事をシェア

CodeZine(コードジン)
https://codezine.jp/article/detail/6783 2012/10/30 14:00

イベント

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

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

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

メールバックナンバー