C# テスト駆動開発の実践方法:GUI部分を実装する #20

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

過去の記事については「C# テスト駆動開発の実践方法」から確認できますので、テスト駆動開発について詳しく知りたい人は参考にしてみてください。前回までにApplicationServiceクラスの実装までを行いました。ここで最後のGUI部分を実装していきます。

Program.csを実装する

さて、このGUI部分についてはテストコードを記述しながら作業を行うことはなく、一気にアプリケーションを完成までもっていきたいと考えています。

必要なものを定義する

最初にアプリケーションに必要なモジュールを作成しておきましょう。このアプリケーションで必要となるのはApplicationServiceモジュールでした。Program.csの冒頭で上記のモジュールを生成しておきます。

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

namespace App03
{
    class Program
    {
        static void Main(string[] args)
        {
            var service = new ApplicationService();
        }
    }
}

メッセージを表示する処理

まずはユーザーに操作を促すメニューを表示する処理を作成していきます。ここは共通で使いまわせそうなのでメソッド化しておきます。

static void ShowMenu()
{
    Console.WriteLine("行いたい操作のメニュー番号を入力してください。");
    Console.WriteLine("1:入金     2:出金     3.終了");
}

static void ShowInstruction()
{
    Console.WriteLine("金額を入力してください。");
}

static void ShowBalance(Money money)
{
    Console.WriteLine($"残高:{ money.Value }円");
}

static void ShowClose()
{
    Console.WriteLine("終了します。");
}

ルーチンを考える

あらかた必要そうなメッセージを表示させる処理を定義したら、メインのルーチンを記述していきます。基本的には「3:終了」が入力されるまでは無限に続くような感じにします。

static void Main(string[] args)
{
    var service = new ApplicationService();

    while(true)
    {
        ShowMenu();

        var input = Console.ReadLine();
        if (input == "3")
        {
            ShowClose();
            Console.ReadLine();
            break;
        }
    }
}

「3」が入力されるようにしておきます。細かい部分はおいておき、大まかな部分の実装だけを済ませておきます。これに引き続いて「1」なら入金、「2」なら出金するような分岐も作っておきます。そして最後は処理後の残高を表示させるようにします。

static void Main(string[] args)
{
    var service = new ApplicationService();

    while(true)
    {
        ShowMenu();
        var input = Console.ReadLine();
        if (input == "1")
        {
            var arg = service.Execute(
                new BankDto()
                {
                    ProcessType = Process.Deposit,
                    Input = new Money(0)
                });
            ShowBalance(arg.Balance);
        }
        if (input == "2")
        {
            var arg = service.Execute(
                new BankDto()
                {
                    ProcessType = Process.Withdraw,
                    Input = new Money(0)
                });
            ShowBalance(arg.Balance);
        }
        if (input == "3")
        {
            ShowClose();
            Console.ReadLine();
            break;
        }
        Console.WriteLine();
    }
}

上記のように実装しておくことで動くようにはなりました。細かい部分の詰めは必要ですが、予定していた処理には近くなったと思います。では実装を続けていきます。

入力を促す処理を作成する

出金・入金ともに必要となるのが「金額の入力」になります。とりあえず金額の入力を促して正しいかどうかを判定するようなメソッドにしておきましょう。

static bool CanConvert(string txt)
{
    return int.TryParse(txt, out var num);
}

次は入力値をMoneyオブジェクトに変換する処理を記述しておきます。

static Money ConvertMoney(string txt)
{
    return new Money(int.Parse(txt));
}

これらをルーチンに組み込みます。少し長いですがこんな感じになるはずです。

static void Main(string[] args)
{
    var service = new ApplicationService();

    while(true)
    {
        ShowMenu();
        var input = Console.ReadLine();
        if (input == "1")
        {
            ShowInstruction();
            var txt = Console.ReadLine();
            if (!CanConvert(txt)) 
            {
                continue;
            }
            var inputMoney = ConvertMoney(txt);
            var arg = service.Execute(
                new BankDto()
                {
                    ProcessType = Process.Deposit,
                    Input = inputMoney
                });
            ShowBalance(arg.Balance);
        }
        if (input == "2")
        {
            ShowInstruction();
            var txt = Console.ReadLine();
            if (!CanConvert(txt))
            {
                continue;
            }
            var inputMoney = ConvertMoney(txt);
            var arg = service.Execute(
                new BankDto()
                {
                    ProcessType = Process.Withdraw,
                    Input = inputMoney,
                });
            ShowBalance(arg.Balance);
        }
        if (input == "3")
        {
            ShowClose();
            Console.ReadLine();
            break;
        }
        Console.WriteLine();
    }
}

一応、これで十分に機能しますね。最後に一気にリファクタリングを行って、メソッドに切り出したりするなどコードをきれいにしておきましょう。最終的にはこんな感じでProgram.csの実装を終わりにしておきます。

using App03.ValueObjects;
using System;

namespace App03
{
    class Program
    {
        static ApplicationService _service;
        static void Main(string[] args)
        {
            _service = new ApplicationService();

            while(true)
            {
                Console.WriteLine();
                var selectedMenu = GetSelectedMenu();
                if (selectedMenu == Menu.Incorrect)
                {
                    Console.WriteLine("正しいメニュー番号を選択してください。");
                    continue;
                }

                if (selectedMenu == Menu.Done)
                {
                    ShowClose();
                    Console.ReadLine();
                    break;
                }

                if (!ProcessSelectedMenu(selectedMenu))
                {
                    Console.WriteLine();
                    continue;
                }
            }
        }

        enum Menu { Incorrect = 0, Deposit = 1, Withdraw = 2, Done = 3 }
        static Menu GetSelectedMenu()
        {
            ShowMenu();
            var input = Console.ReadLine();
            if (int.TryParse(input, out var num))
            {
                switch(num)
                {
                    case 1:
                        return Menu.Deposit;
                    case 2:
                        return Menu.Withdraw;
                    case 3:
                        return Menu.Done;
                    default: 
                        return Menu.Incorrect;
                }
            }
            return Menu.Incorrect;
        }

        static bool ProcessSelectedMenu(Menu menu)
        {
            ShowInstruction();
            var txt = Console.ReadLine();
            if (!CanConvert(txt)) { return false; }
            var inputMoney = ConvertMoney(txt);

            BankDto result = null;
            if (menu == Menu.Deposit)
            {
                result = _service.Execute(
                new BankDto()
                {
                    ProcessType = Process.Deposit,
                    Input = inputMoney
                });
            }
            else if (menu == Menu.Withdraw)
            {
                result = _service.Execute(
                    new BankDto()
                    {
                        ProcessType = Process.Withdraw,
                        Input = inputMoney,
                    });
            }
            if (result != null && string.IsNullOrEmpty(result.Message))
            {
                ShowBalance(result.Balance);
                return true;
            }
            else
            {
                Console.WriteLine(result.Message);
                return false;
            }
        }

        static bool CanConvert(string txt)
        {
            if (int.TryParse(txt, out var num))
            {
                return num >= 0;
            }
            return false;
        }

        static Money ConvertMoney(string txt)
        {
            return new Money(int.Parse(txt));
        }

        static void ShowMenu()
        {
            Console.WriteLine("行いたい操作のメニュー番号を入力してください。");
            Console.WriteLine("1:入金     2:出金     3:終了");
        }

        static void ShowInstruction()
        {
            Console.WriteLine("金額を入力してください。");
        }

        static void ShowBalance(Money money)
        {
            Console.WriteLine($"残高:{ money.Value }円");
        }

        static void ShowClose()
        {
            Console.WriteLine("終了します。");
        }
    }
}

Program.csでの反省点

実装はいったん完成しており問題なく動作することがわかっていますが、よりよくするための改善点はたくさんあります。さっとあげても以下のような改善項目はあげられるでしょう。

  • Program.csでMoneyオブジェクトを扱う必要がない
  • メッセージの出力もApplicationServiceで行うべき
  • Moneyオブジェクトは負の数を許容するべきでない
  • Moneyオブジェクトの引数を文字列型にしてみる

などなど様々にあげられます。こうした内容も本来ならばテストコードを記述しながらリファクタリングを行っていくべきではありますが、ここより先はあなた自身の課題として取り組んでみてください。

本来であればProgram.csはGUIに位置するファイルなので、値の大きさを比べたり、処理のルーチンを持ち込んだりする必要はありませんでした。MVCの観点から言ってもGUIは値を受け取ってControllerやModelといった下層に渡すのみに徹するべきです。

テスト駆動開発のエッセンス

Program.csについては多くの反省点を残してはいますが、ここまでの実装にてテスト駆動開発を実践的に行っていくための方法論は伝えられたのではないかと思っています。テスト駆動開発はテストコードを記述して、実装を行っていく答えありきの実装方法です。

またリファクタリングにも強く、柔軟にソースコードを変更することもできます。テストコードを記述していくということは「変更にも強度のあるソースコードになる」ということの言い換えといえるのではないでしょうか。

とはいえ、ここまでの実装で実践編として伝えてきた内容でテスト駆動開発もいったんは完了を迎えることになります。このエッセンスが、ここまでお付き合い頂いたあなたの役に立つことを祈っています。テスト駆動開発をより習熟していくためには、やはり日頃からテストを意識したコーディングを積み重ねる必要がありますので、そうした入り口として当連載が役立ってもらえたらと思っています。