ApiClientの抽象化から考える、DUnitX&Delphi Mocksを使ったテストコードの実装について

# ハードル高すぎ問題
Delphiでテストコード書こうとしても英語の記事しか見つからず、困ったちゃんになってる人は多いはず...
そんな人のために少しでもハードルを下げられたらなと...

[前の記事](https://qiita.com/ryotuxtuki/items/906207419722384ac2f4)のApplication Service(UseCase)のテストコードを書いていきます

## Delphi Mocksのダウンロード
[このサイト](https://github.com/VSoftTechnologies/Delphi-Mocks/releases)から最新のソースを取得します。ソースコードを任意のパスに配置するため、コードのZIPを取得すればOKです。

# DUnitXの設定
DUnitXを作ってテストを実行するとコンソールが起動します!?
コンソールだといまいち使いづらいため、GUI Test Runnerでテスト可能とする手順を紹介します。

## プロジェクトグループにテストプロジェクトを追加
- プロジェクトグループを右クリックし、[新規プロジェクトを追加]をクリック
- 「Delphiプロジェクト」－「DUnitX」フォルダの「DUnitXプロジェクト」を選択して［OK］をクリック
    - 任意のパスに保存する
- 作成したテストプロジェクトを右クリックし、[オプション]をクリック
    - 「Delphiコンパイラ」のターゲットを「すべての構成」とする
    - 「検索パス」に以下を追加する
    なお、Delphi Mocksのパスはインストールした任意のパスを指定します(プロジェクト直下とかでOK)。
        - $(BDS)\source\DUnitX
        - ..\DelphiMocks\Source

## プロジェクトファイルの修正
デフォではコンソールが起動するため、Debugビルド時にGUI Test Runnerが起動するように修正

### uses節の上部の修正
```pascal:修正前
{$IFNDEF TESTINSIGHT}
{$APPTYPE CONSOLE}
{$ENDIF}
{$STRONGLINKTYPES ON}
```

```pascal:修正後
{$IFNDEF TESTINSIGHT}
{$IFDEF DEBUG}
  {$APPTYPE GUI}
{$ELSE}
  {$APPTYPE CONSOLE}
{$ENDIF}
{$ENDIF}{$STRONGLINKTYPES ON}
```

### uses節の追加
以下を追加します。
```pascal
Vcl.Forms,
DUnitX.Loggers.GUI.VCL,
```

### begin句以降の追加
以下を追加します。
```pascal
{$IFDEF DEBUG}
  ReportMemoryLeaksOnShutdown := True; // デバッグ時はメモリリークを検出する
{$ENDIF}

{$IFDEF DEBUG}
  Application.Initialize;
  Application.MainFormOnTaskbar := True;
  Application.CreateForm(TGUIVCLTestRunner, GUIVCLTestRunner);
  Application.Run;
  Exit;
{$ENDIF}
```

これでプロジェクトを実行することでGUI Test Runnerが起動するはずです。

# テストコードを書く
やっとこさテストコード書く準備できたので、書いていく。
前の記事のGetSettingsUseCaseU.pasに対して書いていきます。

## テストケースの洗いだし
テストコード書くにも、まずテストケースを洗い出さなきゃ。
大枠で以下と思います。
- トークンの取得に成功
    - 設定情報の取得成功
        - 戻りに「0」を返すか？
        - 設定情報の戻りが期待どおりか？
    - 設定情報の取得失敗
        - 戻りに「1」を返すか？
        - エラーログが出力されるか？
        メッセージは「トークン取得に失敗」か？
- トークンの取得に失敗
    - 戻りに「1」を返すか？
    - エラーログが出力されるか？
    メッセージは「トークン取得に失敗」か？
    - FApiClient.GetSettingsが呼ばれないか？

## テストコードのスケルトン
上記テストケースをもとにテストコードのスケルトンを作成。

```pascal:GetSettingsUseCaseTestU.pas
unit GetSettingsUseCaseTestU;

interface

uses
  DUnitX.TestFramework;

type
  [TestFixture]
  TGetSettingsUseCaseTest = class
  public
    // 以下にテストケースを書いていく
    // テストケース名は任意だが、テストしたい関数名を先頭にして、テスト内容を
    // 日本語で書くと分かりやすいかも？？？
    [Test]
    procedure Execute_トークン取得APIの実行に失敗時に戻りは1か;
    [Test]
    procedure Execute_トークン取得APIの実行に失敗時にエラーが出力されるか;
    [Test]
    procedure Execute_トークン取得APIの実行に失敗時にGetSettingsが呼ばれないか;

    [Test]
    procedure Execute_設定情報の取得成功時の戻りが期待どおりか;
    [Test]
    procedure Execute_設定情報の取得失敗時の失敗時にエラーが出力されるか;
  end;

implementation

uses
  GetSettingsUseCaseU,
  // Delphi.Mocksを使うなら、以下をusesに追加
  Delphi.Mocks;

{ TGetSettingsUseCaseTest }

procedure TGetSettingsUseCaseTest.Execute_トークン取得APIの実行に失敗時にGetSettingsが呼ばれないか;
begin

end;

procedure TGetSettingsUseCaseTest.Execute_トークン取得APIの実行に失敗時にエラーが出力されるか;
begin

end;

procedure TGetSettingsUseCaseTest.Execute_トークン取得APIの実行に失敗時に戻りは1か;
begin

end;

procedure TGetSettingsUseCaseTest.Execute_設定情報の取得失敗時の失敗時にエラーが出力されるか;
begin

end;

procedure TGetSettingsUseCaseTest.Execute_設定情報の取得成功時の戻りが期待どおりか;
begin

end;

initialization
  // めんどくさいが、ここの追加を忘れない...
  TDUnitX.RegisterTestFixture(TGetSettingsUseCaseTest);

end.
```

## Delphi Mocksの機能で任意の値を返すモックを作成
以下の要領で実装していきます。
```pascal
procedure TGetSettingsUseCaseTest.Execute_トークン取得APIの実行に失敗時に戻りは1か;
begin
  // 1.ApiClientのMockを作成
  var LApiClientMock: TMock<IApiClient> := TMock<IApiClient>.Create;

  // 2.LoggerのMockを作成
  var LLogger: TMock<ILogger> := TMock<ILogger>.Create;

  // 3.作ったMock(依存性)をコンストラクタで注入
  var LGetSettingsUseCase: TGetSettingsUseCase := TGetSettingsUseCase
    .Create(LApiClientMock, LLogger);

  // 4.GetToken関数が返す値を定義(失敗したときの動きを定義する)
  var LTokenResult: TApiResult := TApiResult.Create(False, 'Fail', 'トークン取得に失敗', 500, 2010);
  try
    // 5.ApiClientのGetToken関数が↑で定義した値を返すものとする
    LApiClientMock.Setup.WillReturn(LTokenResult).When.GetToken(
      It(0).IsAny<string>(),   // 第一引数に来る値は何でもOK
      It(1).IsAny<string>(),   // 「（）」内の数値は何番目の引数かを意味する
      It(2).IsAny<string>());  // 引数の値と結果を指定したい場合、特定の値を入れる

    var LResult: string;
    // 6.UseCaseを実行する。引数は↑の値のとおり、任意でOK。
    var LActual: Integer := LGetSettingsUseCase.Execute('hoge', 'fuga', LResult);

    // 7.DUnitXの機能で戻りが期待どおりかを検証する
    Assert.AreEqual(LActual, 1);
  finally
    // 8.↑で確保したメモリは解放しないと、Test Runnerを閉じた時にエラーが表示される
    LApiClientMock.Free;
    LLogger.Free;
    LGetSettingsUseCase.Free;
  end;
end;
```

5.で記載した感じで特定の関数が何を返すかを定義できる(差替え可能となる)。Delphi Mocksの内部でRTTIが使われており、候補がでてきて使いやすかった。
テスト対象の関数(今回ならGetToken)の引数の値が何でもOKなら、「It(n).IsAny<T>()」で指定する。

## Delphi Mocksの機能で関数の呼び出し有無を検証する
テストを考えるとき、次の処理が呼ばれず期待どおりに抜けているか検証したい場合がある。その際の実装イメージは以下。
今回のテストケースでは、トークン取得(GetToken関数の実行)に失敗した場合、後続のGetSettings関数が呼ばれないことを検証したい。

```pascal
procedure TGetSettingsUseCaseTest.Execute_トークン取得APIの実行に失敗時にGetSettingsが呼ばれないか;
begin
  var LApiClientMock: TMock<IApiClient> := TMock<IApiClient>.Create;
  var LLogger: TMock<ILogger> := TMock<ILogger>.Create;

  var LGetSettingsUseCase: TGetSettingsUseCase := TGetSettingsUseCase
    .Create(LApiClientMock, LLogger);
  var LTokenResult: TApiResult := TApiResult.Create(False, 'Fail', 'トークン取得に失敗', 500, 2010);
  try
    // 1.ApiClientのGetToken関数が↑で定義した値(エラー)を返すものとする
    LApiClientMock.Setup.WillReturn(LTokenResult).When.GetToken(
      It(0).IsAny<string>(),
      It(1).IsAny<string>(),
      It(2).IsAny<string>());

    // 2.GetSettings関数はトークン取得に失敗した場合呼ばれないかの検証
    LApiClientMock.Setup.Expect.Never.When.GetSettings(
      It(0).IsAny<string>(),  // 引数を意識しないなら、「It(n).IsAny<T>()」でOK
      It(1).IsAny<string>());

    var LResult: string;
    // 3.UseCaseを実行
    var LActual: Integer := LGetSettingsUseCase.Execute('hoge', 'fuga', LResult);

    Assert.AreEqual(LActual, 1);
    // 4.2.でExpectした内容が期待どおりか検証する
    LApiClientMock.Verify;
  finally
    LApiClientMock.Free;
    LLogger.Free;
    LGetSettingsUseCase.Free;
  end;
end;
```

# まとめ
C#のMoq(NSubstituteでもOK)を使ったことがある人なら、似たような使用感かと思います。テストコードを書く前は「DelphiでもC#のようにかけるのか？？？」と不安でしたが、基本的には同じ要領で作成可能でした。
テストコードを書くことで修正がどんな影響を及ぼすのかの怯えを最小限にできました。