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

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

過去の記事については「C# テスト駆動開発の実践方法」から確認できますので、テスト駆動開発について詳しく知りたい人は参考にしてみてください。前回はアプリケーションサービスの初期時点での処理を実行させるためのメソッドを作成しました。今回は各機能のアプリケーションサービスを作成します。

テストコードを実装する

では早速、テスト駆動開発のセオリーに従ってテストコードから実装をしていきましょう。今回は預金処理ということで画面から入金金額と処理区分を受け取って、その処理区分に応じた処理を行うような設計としますので、テストコードは以下のようになると思います。

[TestMethod]
public void DepositTest_OK()
{
    //初期残高
    string writePath = @"C:\temp\balance.txt";
    string text = ((int)0).ToString();
    File.WriteAllText(writePath, text);
    
    //入金処理
    var app = new ApplicationService();
    var arg = app.Execute(
        new BankDto()
        {
            ProcessType = Process.Deposit,
            Input = new Money(100),
        });

    //預金の確認
    Assert.IsTrue(arg.Balance.Equals(new Money(100)));
}

初期残高は0円として記録しておき、預金で100円の入金を行い、最新の残高が100円になるかを確認するようなテストコードとしています。預金の場合は出金のような制限がないため上記のテストコードで十分かと思います。

またついでにDeposit(預金)のほかにもWithdraw(出金)側のテストコードも実装しておきましょう。預金の場合と同じような感じで実装できるかと思います。OKパターンに加えてNGパターンのテストコードも実装しておきます。

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

    //入金処理
    var app = new ApplicationService();
    var arg = app.Execute(
        new BankDto()
        {
            ProcessType = Process.Withdraw,
            Input = new Money(10),
        });

    //預金の確認
    Assert.IsTrue(arg.Balance.Equals(new Money(90)));
}

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

    //入金処理
    var app = new ApplicationService();
    var arg = app.Execute(
        new BankDto()
        {
            ProcessType = Process.Withdraw,
            Input = new Money(101),
        });

    //メッセージと残高の確認
    Assert.AreEqual(arg.Message, "残高以上の引き出しはできません。");
    Assert.IsTrue(arg.Balance.Equals(new Money(100)));
}

とりあえずは上記のようにテストコードを記述しておきます。100円から10円引いて残高が90円になることに確認と100円から101円を引いて出金できないパターンの2パターンです。最低限のパターンは網羅できていると思います。

コンパイルエラーからレッドにする

テストコードの実装が完了したのでコンパイルエラーを解消するために修正を加えます。基本的にはApplicationSeriviceクラスに実装を加えていきます。

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

namespace App03
{
    public class ApplicationService
    {
        private readonly IRepository _repos;
        private readonly BankService _service;
        public ApplicationService()
        {
            string path = Constants.DbPath;
            this._repos = new BankRepository(path);
            this._service = new BankService(_repos);
        }

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

        public BankDto Execute(BankDto bankDto)
        {
            throw new NotImplementedException();
        }
    }

    public enum Process
    {
        Deposit, Withdraw
    }

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

        public Process ProcessType { get; set; }

        public Money Balance { get; set; }

        public string Message { get; set; }
    }
}

ApplicationServiceクラスを上記のように修正します。するとコンパイルエラーは解消されると思います。処理内容の正しさはおいてき、まずはコンパイルエラーからレッドになる状態を確認します。実装が出来たらテストを実施して状態を確認しましょう。

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

追加したテストがレッドになることを確認できたので、ここからテストをグリーンにする作業に移っていきます。まずは預金処理の場合をコーディングしていきましょう。

預金処理の実装を行う

預金処理は基本的にBankServiceクラスのAddBalanceメソッドを呼び出すだけになります。とはいえ、処理タイプとしてenumを定義しているので、その処理に応じて分岐させるようにしておきます。

public BankDto Execute(BankDto bankDto)
{
    if (bankDto.ProcessType == Process.Deposit)
    {
        var balance = _service.AddBalance(bankDto.Input);
        return new BankDto()
        {
            Balance = balance
        };
    }
    return new BankDto();
}

Executeメソッドの中を上記のようにしておきましょう。ソースコードが洗練されていなくても気にせず、まずはグリーンにするための処理を記述します。実装できたらテストを実施してDepositTest_OKがグリーンになることを確認します。

出金処理の実装を行う

預金処理のコーディングが完了したので、次は出金処理の実装に移ります。Executeメソッドに処理をまとめているので、実装箇所は預金処理の場合と変わりません。

public BankDto Execute(BankDto bankDto)
{
    if (bankDto.ProcessType == Process.Deposit)
    {
        var balance = _service.AddBalance(bankDto.Input);
        return new BankDto()
        {
            Balance = balance
        };
    }
    else if (bankDto.ProcessType == Process.Withdraw)
    {
        var balance = _service.Withdraw(bankDto.Input);
        return new BankDto()
        {
            Balance = balance
        };
    }
    return new BankDto();
}

さてOKパターンのテストをグリーンにするために実装を追加してみました。この状態でいったんテストを実施してグリーンになるかを確認します。WithdrawTest_OKは問題なかったでしょうか。

出金処理の異常パターンを実装する

さて、出金処理については正常パターンの実装までが完了したところです。次にやることは出金処理・預金処理のテスト結果を壊さずに異常パターンをグリーンになるように実装することになります。

テスト駆動開発ではすでに実行したテストを行いながらリファクタリングできるのが強みですので、今回もテストを実施しながら、大胆に、かつ柔軟にリファクタリングを行っていきましょう。

public BankDto Execute(BankDto bankDto)
{
    try
    {
        if (bankDto.ProcessType == Process.Deposit)
        {
            var balance = _service.AddBalance(bankDto.Input);
            return new BankDto()
            {
                Balance = balance
            };
        }
        else if (bankDto.ProcessType == Process.Withdraw)
        {
            var balance = _service.Withdraw(bankDto.Input);
            return new BankDto()
            {
                Balance = balance
            };
        }
        return new BankDto();
    }
    catch (Exception ex)
    {
        return new BankDto()
        {
            Balance = _service.GetBalance(),
            Message = ex.Message,
        };
    }
}

今回はWithdrawメソッドにて出金指示が残高を上回る場合について、例外を送出するようにしていたので、大胆にtry ~ catch ~で囲うようにしておき、例外が発生した場合は現状の残高を再取得してメッセージと一緒に返却するようにしています。この状態でテストを通せば問題なくなるはずですので、テストを実施してみてください。

ここまで来るとアプリケーションの大枠がだいぶ見えてきたんじゃないかと思います。もう一息でアプリケーションが完成するところまで来ました。これまで多くのテストコードを記述してきたと思いますが、少し慣れてきたでしょうか。最後に作成するProgram.csはGUI部分に該当するため、テストコードを記述することなく実装を進めていきます。