.NET環境で使えるHTTP API動作を模倣するツールWireMock.Netを紹介する

# 概要
* WireMock.NetというHTTP APIの動作を模倣するツールを紹介する。
* WireMock.Netを利用した単体テストの簡単なサンプルを示す。
* WireMock.Netを利用したコンソールアプリの簡単なサンプルを示す。
* 最後に、参考にした情報をまとめる。

# WireMock.Netとは？
HTTP APIの動作を簡単に模倣できるツールである。
元々はJavaで利用可能なモックツール[WireMock][1]があり、これを.NET用にC#で模倣したものである。
実行可能な環境はローカル、IIS、Azure、Dockerなど多岐に渡る。

参考：[What Is WireMock.Net][3]

下記ような場面で活用が可能であると考える。

<b>(1)ユニットテストや結合テスト</b>
- HTTP通信を模倣できるため、HTTP通信を行うクラスの単体テストが可能になる。
    - HTTP通信を単体テストする方法として、下記リンクのようにMoqを用いてHttpMessageHandlerをモック化する手法がある。
    この手法に比べ、WireMock.Netは機能が豊富なため、評価できることが多い。テストコードの実装もシンプルに記述できる。
[HttpClientをMoqでモック化する](https://tnakamura.hatenablog.com/entry/2020/10/28/how-to-mock-http-client-using-moq)

- HTTP応答の内容を自由に設定できるため、HTTP応答に応じた画面遷移の評価などが容易になる。
個人的には、通信遅延が発生したケース、異常なステータス(HTTP 4xx または 5xx)になったケースの評価を楽にできる点が素晴らしいと思っています。

<b>(2)HTTP APIのサーバが未実装な場合</b>
HTTP API仕様書に基づき、モックサーバをWireMockで作ることにより、サーバの実装完了を待たずに、クライアントの実装を進められる。



# 基本的な使い方

## サーバーを起動する方法

~~~C#
using WireMock.Server;

var server = WireMockServer.Start(int? port = 0, bool ssl = false);
~~~

引数として、ポート番号(port)・SSLを使用するか(ssl)を指定できる。本記事ではSSLを使用しません。
ポート番号を指定しない場合は、localhostに割り当てられたランダムなポート番号でサーバーが起動する。
ポート番号を指定した場合は、指定したポート番号で起動する。

:::note warn
指定したポート番号がOSが使用している場合、実行時エラーになります。
:::


## HTTP要求とHTTP応答をマッピングする方法

HTTP要求・HTTP応答の組み合わせを登録する必要がある。

下記のWeb APIを想定した場合の実装例を示す。

| HttPメソッド | URL |　応答ステータス　|　応答Body |　ヘッダー情報　|　補足 |
|:-|:-|:-|:-|:-|:-|
|GET  |http://localhost:xxxxx/foo|200|{"msg":"Hello world!"}|Content-Type: application/json|xxxxxはport番号。<br>応答に1秒かかる。|


~~~C#
var server = WireMockServer.Start();
const string expected = @"{ ""msg"": ""Hello world!"" }";
server 
    .Given(Request.Create().WithPath("/foo").UsingGet())
    .RespondWith(
        Response.Create()
            .WithStatusCode(HttpStatusCode.OK)
            .WithHeader("Content-Type", "application/json")
            .WithBody(expected)
            .WithDelay(TimeSpan.FromSeconds(1)));
~~~

Given()の引数にHTTP要求情報(URL/HTTP メソッドなど）を記述する。
RespondWith()の引数に、HTTP要求に対応する応答情報（応答ステータス/ヘッダー/ボディー/通信遅延など）を記述する。

## サーバーを終了する方法

~~~C#
serve.Stop();
~~~


# 実行環境

<details><summary>実行環境</summary><div>

フレームワーク
　.NET Framework 4.6.2
Nugetパッケージ
　WireMock.NET 1.4.19
　NUnit 3.12.0
　NUnit3TestAdapter 3.16.1
　Microsoft.NET.Test.Sdk 16.5.0
　
</div></details>


# WireMock.Netを用いた単体テスト
実際にWireMock.Netを利用して、単体テストの簡単なサンプルを実行する。

## 1.テストプロジェクトを作成する
[NUnit3 テストプロジェクト]->ターゲットフレームワークを選択する
今回は.NET Framework4.6.2で作成。

## 2.WireMock.NETを追加する
[Nugetパッケージの管理]を起動する。[WireMock.Net]をインストールする。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/156594/dbf93276-a50d-5f07-51f9-89f05125e853.png)

## 3.テストコードを作成する


### テストコード

~~~C#
using System.Net;
using System.Net.Http;
using System.Threading.Tasks;
using NUnit.Framework;
using WireMock.RequestBuilders;
using WireMock.ResponseBuilders;
using WireMock.Server;

namespace TestWireMock
{
    public class Tests
    {
        private WireMockServer _mockServer;
        private static HttpClient _client;

        [SetUp]
        public void Setup()
        {
            _mockServer = WireMockServer.Start();
            _client = new HttpClient();
        }

        [Test]
        public async Task MockHttpApi_Foo_CaseStatusOk()
        {
            // Arrange WireMock.Net serverの設定
            const string expected = @"{ ""msg"": ""Hello world!"" }";
            _mockServer
                .Given(Request.Create().WithPath("/foo").UsingGet())
                .RespondWith(
                    Response.Create()
                        .WithStatusCode(HttpStatusCode.OK)
                        .WithHeader("Content-Type", "application/json")
                        .WithBody(expected));

            var port = _mockServer.Ports;
            // Act HTTPサーバーのURLにアクセスして、HTTP応答を取得する
            var response = await _client.GetAsync($"{_mockServer.Urls[0]}/foo");
            var result = await response.Content.ReadAsStringAsync();

            //Assert
            Assert.AreEqual(expected, result);
        }

        [TearDown]
        public void ShutdownServer()
        {
            _mockServer.Stop();
        }
    }
}
~~~

### テストコードを記載する上での注意点

:::note info
本項目の記載は、[Using WireMock in UnitTests][5]の記述を意訳したものです。
:::

<b>(1)ハードコードでポートを指定しないこと</b>

ハードコードで指定したポートがOS上で空いていることが100%保証されていないため、ユニットテストを実行する際に問題を引き起こす可能性がある。
WireMock.Netにポートを動的に選択させること。

なお、WireMock.Netからポート番号は下記で取得できる。
WireMockServer.Ports or WireMockServer.Urls

<b>(2)テスト終了時に後処理を行うこと</b>

各テストの終了時には、リクエストログをクリーンアップするか、サーバをシャットダウンする。



## 4.単体テストを実行する
テストを実行して、[Failed]にならないことを確認する。

# WireMock.Netを用いたコンソールアプリケーション

## ソースコード

~~~C#
using System;
using System.Net;
using WireMock.Exceptions;
using WireMock.Logging;
using WireMock.RequestBuilders;
using WireMock.ResponseBuilders;
using WireMock.Server;
using WireMock.Settings;

namespace WireMockStandAlone
{
    class Program
    {
        static void Main( string[] args )
        {
            // WireMockServerは、IDisposableを継承している。リソース解放の構文としてusingを使用できるが、try-finallyで記述する。
            // 理由は、終了処理に非同期処理が含まれているためである。STOP()は非同期処理の完了を待つが、Dispose()は完了を待たない。
            WireMockServer server = null;
            try
            {
                var port = 8080;
                server = WireMockServer.Start(
                    new WireMockServerSettings()
                    {
                        Port = port,
                        Logger = new WireMockConsoleLogger()
                    });
                Console.WriteLine($"{nameof(WireMockServer)} Running at {nameof(port)} : {port}");
                const string expected = @"{ ""msg"": ""Hello world!"" }";
                server 
                    .Given(Request.Create().WithPath("/foo").UsingGet())
                    .RespondWith(
                        Response.Create()
                            .WithStatusCode(HttpStatusCode.OK)
                            .WithHeader("Content-Type", "application/json")
                            .WithBody(expected)
                            .WithDelay(TimeSpan.FromSeconds(1)));

                Console.WriteLine("Press any key to stop...");
                Console.ReadKey(true);
            }
            catch (WireMockException e)
            {
                Console.WriteLine(e);
            }
            finally
            {
                server?.Stop();
            }
        }
    }
}
~~~

## 実行結果

### 起動時のコンソール出力

<details><summary>起動時のコンソール出力</summary><div>

~~~
2022/02/11 11:02:48 [Info] : By Stef Heyenrath (https://github.com/WireMock-Net/WireMock.Net)
2022/02/11 11:02:48 [Debug] : Server settings {
  "Port": 8080,
  "UseSSL": null,
  "StartAdminInterface": null,
  "ReadStaticMappings": null,
  "WatchStaticMappings": null,
  "WatchStaticMappingsInSubdirectories": null,
  "ProxyAndRecordSettings": null,
  "Urls": null,
  "StartTimeout": 10000,
  "AllowPartialMapping": null,
  "AdminUsername": null,
  "AdminPassword": null,
  "AdminAzureADTenant": null,
  "AdminAzureADAudience": null,
  "RequestLogExpirationDuration": null,
  "MaxRequestLogCount": null,
  "CorsPolicyOptions": null,
  "AllowCSharpCodeMatcher": null,
  "AllowBodyForAllHttpMethods": null,
  "AllowOnlyDefinedHttpStatusCodeInResponse": null,
  "DisableJsonBodyParsing": null,
  "DisableRequestBodyDecompressing": null,
  "HandleRequestsSynchronously": null,
  "ThrowExceptionWhenMatcherFails": null,
  "CertificateSettings": null,
  "CustomCertificateDefined": false,
  "WebhookSettings": null,
  "UseRegexExtended": true,
  "SaveUnmatchedRequests": null
}
2022/02/11 11:02:48 [Info] : Server using .NET Framework 4.6.1 or higher
WireMockServer Running at port : 8080
Press any key to stop...

~~~

</div></details>

### HTTP要求を受信した時のコンソール出力

VSCodeのREST Clientを用いて、http要求を送信する。
使い方は[こちら](https://qiita.com/toshi0607/items/c4440d3fbfa72eac840c)を参考にしてください。

~~~ test.http
GET http://localhost:8080/foo HTTP/1.1
Content-Type: application/json
~~~

<details><summary>HTTP要求を受信した時のコンソール出力</summary><div>

~~~
2022/02/11 11:06:36 [DebugRequestResponse] : Admin[False] {
  "Guid": "9639e3e0-b4e7-428f-9017-46bb072f9f15",
  "Request": {
    "ClientIP": "127.0.0.1",
    "DateTime": "2022-02-11T11:06:35.4409977Z",
    "Path": "/foo",
    "AbsolutePath": "/foo",
    "Url": "http://localhost:8080/foo",
    "AbsoluteUrl": "http://localhost:8080/foo",
    "ProxyUrl": null,
    "Query": {},
    "Method": "GET",
    "Headers": {
      "Connection": [
        "close"
      ],
      "Content-Type": [
        "application/xml"
      ],
      "Accept-Encoding": [
        "gzip, deflate"
      ],
      "Host": [
        "localhost:8080"
      ],
      "User-Agent": [
        "vscode-restclient"
      ]
    },
    "Cookies": null,
    "Body": null,
    "BodyAsJson": null,
    "BodyAsBytes": null,
    "BodyEncoding": null,
    "DetectedBodyType": null,
    "DetectedBodyTypeFromContentType": null
  },
  "Response": {
    "StatusCode": 200,
    "Headers": {
      "Content-Type": [
        "application/json"
      ]
    },
    "BodyDestination": "SameAsSource",
    "Body": "{ \"msg\": \"Hello world!\" }",
    "BodyAsJson": null,
    "BodyAsBytes": null,
    "BodyAsFile": null,
    "BodyAsFileIsCached": null,
    "BodyOriginal": null,
    "BodyEncoding": {
      "CodePage": 65001,
      "EncodingName": "Unicode (UTF-8)",
      "WebName": "utf-8"
    },
    "DetectedBodyType": 1,
    "DetectedBodyTypeFromContentType": 0,
    "FaultType": null,
    "FaultPercentage": null
  },
  "MappingGuid": "a6976d50-0dc5-449d-81b8-70b36f865564",
  "MappingTitle": null,
  "RequestMatchResult": {
    "TotalScore": 2.0,
    "TotalNumber": 2,
    "IsPerfectMatch": true,
    "AverageTotalScore": 1.0,
    "MatchDetails": [
      {
        "Name": "PathMatcher",
        "Score": 1.0
      },
      {
        "Name": "MethodMatcher",
        "Score": 1.0
      }
    ]
  },
  "PartialMappingGuid": "a6976d50-0dc5-449d-81b8-70b36f865564",
  "PartialMappingTitle": null,
  "PartialRequestMatchResult": {
    "TotalScore": 2.0,
    "TotalNumber": 2,
    "IsPerfectMatch": true,
    "AverageTotalScore": 1.0,
    "MatchDetails": [
      {
        "Name": "PathMatcher",
        "Score": 1.0
      },
      {
        "Name": "MethodMatcher",
        "Score": 1.0
      }
    ]
  }
}
~~~

</div></details>


# あとがき
WireMock.Netの使い方を学ぶには、下記Wikiが参考になります。
[WireMock.Net Wiki][4]


# 参考
| No | リンク | 備考 |
|:-|:-|:-|
|1   |[WireMock][1]|WireMockの公式ページ   |
|2   |[WireMock.Net][2]|GitHub WireMock.Netのページ。概要・機能・関連ページへのリンクがある。|
|3   |[What Is WireMock.Net][3]   |WireMock Wiki Page:What Is WireMock.Net|
|4   |[WireMock.Net Wiki][4]   |WireMock Wiki Page:Home|
|5   |[Using WireMock in UnitTests][5]   |WireMock.Netの単体テストのページ<br>[WireMockServerのテストコード](https://github.com/WireMock-Net/WireMock.Net/blob/master/test/WireMock.Net.Tests/WireMockServerTests.cs#L50)も参考になる。|
|6   |[Writing robust integration tests in .NET with WireMock.NET][6]   |YoutubeでWireMock.Netを紹介してる動画(英語)|
|7|[WireMock.Net for better Integration Tests][7]|WireMock.Netを紹介したブログ。|

[1]:http://wiremock.org/
[2]:https://github.com/WireMock-Net/WireMock.Net
[3]:https://github.com/WireMock-Net/WireMock.Net/wiki/What-Is-WireMock.Net
[4]:https://github.com/WireMock-Net/WireMock.Net/wiki
[5]:https://github.com/WireMock-Net/WireMock.Net/wiki/Using-WireMock-in-UnitTests
[6]:https://www.youtube.com/watch?v=YU3ohofu6UU
[7]:https://angela-evans.com/wiremock-net-for-better-integration-tests/