C# テスト駆動開発の実践方法:アプリケーションサービスを実装する1 #18

当連載ではテスト駆動開発を実践するために必要な基礎知識や実践方法を解説しています。現在は実践編として簡単なアプリケーションを作成しながらテスト駆動開発のやり方を解説しています。

過去の記事については「C# テスト駆動開発の実践方法」から確認できますので、テスト駆動開発について詳しく知りたい人は参考にしてみてください。前回までに必要だと思える個別機能の実装は完了しているので、これを組み合わせてアプリケーションを実行させるようにしていきます。

アプリケーションサービスクラスを作成する

前回までにBankServiceを作成してきました。このBankServiceは「口座」に関連する処理・機能を提供するモジュールとして実装をしてきました。したがってアプリケーションの全体の流れを管理するような設計にはなっておりませんでした。

ここで作成したいのが「アプリケーションサービスクラス」と呼ばれるクラスです。簡単に言ってしまうと「各機能を組み合わせてアプリケーションの全体の流れを作るクラス」ということになります。このファイルを作成していきます。まずはApp03のプロジェクトを右クリックして「追加」から「クラス」を選択して、クラス名を「ApplicationService」としてファイルを作成しましょう。とりあえず出来たらここまでにしておきます。

機能・実装方針を考えよう

さて口座の仕様はBankServiceクラスなどで定義されていますので、これらをどう組み合わせるかを考えることにしていきます。今回作成したいアプリケーションの大きな流れは以下のようになります。

  • 残高を表示する
  • メニューを表示する
  • ユーザーに選択を促す
  • メニューで指定された処理を行う(※)
  • 続けて入力するかを決める

という感じです。注釈の部分はもう少し細かく決める必要があります。というのも選択したメニューによって挙動が異なるからです。もう少し細かく見ていきます。

  • 入金・出金:金額の入力を促して処理結果を表示し、最新残高を表示する

どちらも金額の入力を促して、処理を実行して最新の残高を表示するところは一緒ですが、処理を実行する場合に様々な制限が加わりますよね。たとえば以下のような感じです。

  • 数字で入力されているか
  • 金額はプラス値であるか
  • 出金金額は正常であるか

基本的にはこのようにあげられると思います。あとは上記をどのように実装するかです。まず大きな処理を考えるとすると以下の形式が思い浮かびます。

  1. 画面の入力値を受け取って、すべての処理を一つのメソッドで管理する
  2. メニューごとにpublicメソッドを作成して処理を管理する

処理の今回はコンソールアプリケーションですが、この画面がコンソールではなくWPFやWebだった場合を考慮して拡張性のある方針にしたいと思います。なのでApplicationServiceの入り口は1つに絞り、中で処理を分岐させて一つのメソッドで処理を管理する方針とします。

画面の入力項目を考える

それではApplicationServiceクラスの実装に移りたいと考えますが、その前に画面の入力項目を想定しておきたいと思います。ひとつ前に記載した「ApplicationServiceの入り口は1つに絞り、中で処理を分岐させて一つのメソッドで処理を管理する方針」とするためには入り口で想定されるインプット・アウトプットを決めておく必要がありそうです。

インプットとして考えられるのは「選択されたメニュー」「入力された金額」になるかなと思います。逆にアウトプットは「最新残高」と「メッセージ」になるでしょうか。いったんはこの想定で考えてみましょう。ということで早速、テストクラスから実装を開始します。

テストクラスを実装する

今回は新しくApplicationServiceクラスを作成したので、それらをテストするためのクラスを新しく作成します。いつものごとくBankAccountTestプロジェクトを右クリックして「追加」から「単体テスト」を選択してください。名称がUnitTest2になっているので、ソリューションエクスプローラーでファイルを選択して右クリックして「ApplicationServiceTest」と変更しましょう。以下のようになっていればOKです。

using Microsoft.VisualStudio.TestTools.UnitTesting;
using System;

namespace BankAccountTest
{
    [TestClass]
    public class ApplicationServiceTest
    {
        [TestMethod]
        public void TestMethod1()
        {
        }
    }
}

初期時点の処理

まずは初期時点のメッセージやメニュー表示はProgram.csで行う想定なので初期時点では最新残高だけ表示できればOKになると思います。という訳でテストメソッドを記載していきます。デフォルトのTestMethod1を削除して新しくメソッドを作成しましょう。

[TestMethod]
public void InitializeTest()
{
    string writePath = @"C:\temp\balance.txt";
    string text = ((int)100).ToString();
    File.WriteAllText(writePath, text);

    var app = new ApplicationService();
    var args = app.Initialize();

    //初期残高の確認
    Assert.IsTrue(args.Balance.Equals(new Money(100)));
}

現状ではApplicationServiceは認識されていない状況です。というのも「アプリケーションサービスクラスを作成する」の項目で作成したApplicationServiceクラスの識別子がinternalだからですね。まずは上記の状態をコンパイルエラーからレッドにするように修正してみましょう。ApplicationServiceクラスから変更していきます。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace App03
{
    public class ApplicationService
    {
        public object Initialize()
        {
            throw new NotImplementedException();
        }
    }
}

いったんはApplicationServiceクラスを上記のように変更しています。またApplicationServiceTestクラスから各機能を見ることができるようにusingも定義追加してください。現在は以下の3つを追加しています。

using App03;
using App03.ValueObjects;
using System.IO;

この状態でコンパイルエラーが解消されるかと思いきや、初期残高の確認部分でコンパイルエラーが生じていましたので解消していきます。ApplicationServiceクラスにデータを移送するためだけのクラスを定義しておきましょう。ApplicationServiceの全貌は以下のようになります。

using App03.ValueObjects;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace App03
{
    public class ApplicationService
    {
        public object Initialize()
        {
            throw new NotImplementedException();
        }
    }

    public class BankDto
    {
        public Money Balance { get; set; }
    }
}

現状でもまだコンパイルエラーは解消されません。これはInitializeメソッドの戻り値がobjectだからですね。これを先ほど作成したBankDtoに変更します。こうすることでコンパイルエラーが消えました。いったんレッドになるかを確認します。

レッドからグリーンにする

さてレッドになるところまで確認ができたので、これらをグリーンにするように修正を加えましょう。そこまで難しくないと思いますので頑張ってチャレンジしてみてください。

public BankDto Initialize()
{
    string path = @"C:\temp\balance.txt";
    IRepository _repos = new BankRepository(path);
    var _service = new BankService(_repos);

    return new BankDto()
    { 
        Balance = _service.GetBalance(),
    };
}

きれいではありませんが、いったん上記のようにしておきます。リファクタリングは後からしますので、まずは目の前のテストをグリーンにするための実装に集中します。初期時点でのテストとしてはグリーンになることが確認できれば十分です。

リファクタリングをする

グリーンになることを確認したら、次はリファクタリングを行います。先ほどのメソッドでは気になる点がたくさんありました。repositoryやserviceクラスがメソッド内の変数として定義されているので、ほかのメソッドで何回も生成しなくてはなりません。コンストラクタもないですし、そのあたりは直したいです。

public class ApplicationService
{
    private readonly IRepository _repos;
    private readonly BankService _service;
    public ApplicationService()
    {
        string path = @"C:\temp\balance.txt";
        this._repos = new BankRepository(path);
        this._service = new BankService(_repos);
    }

    public BankDto Initialize()
    {
        return new BankDto()
        { 
            Balance = _service.GetBalance(),
        };
    }
}

ApplicationServiceクラスを上記のようにリファクタリングしました。テストを実行してグリーンであることを確認します。次に気になる点はパスがそのまま埋め込まれていることです。これを外部の定数クラスに切り出します。新規のConstantsファイルを、ApplicationService.csやProgram.csと同列に作成して以下のようにしておきます。今回はstaticの静的エリアに配置するようにして、いつでも呼び出せるようにしておきましょう。

namespace App03
{
    public static class Constants
    {
        public const string DbPath = @"C:\temp\balance.txt";
    }
}

上記のようにすることによって、ApplicationServiceクラスの記述内容も変わってきます。コンストラクタを以下のように修正します。できれば設定値情報などはファイルに決め打ちで記述するのではなく、一か所にまとめておくほうがきれいですよね。

public ApplicationService()
{
    string path = Constants.DbPath;
    this._repos = new BankRepository(path);
    this._service = new BankService(_repos);
}

再度テストを実行して無事にリファクタリングが完了しているかを確認します。すべてグリーンであれば問題ありませんので、いったんはここで終了です。初期時点の処理を実装完了したので、次は大きなメインルーチンの処理を管理するメソッドを実装していきます。