FizzBuzzで、自動テスト、依存性の注入、Swaggerドキュメント 全部やる

## 作るもの

- `FizzBuzz`を返す`Web API`

```:イメージ
こんなリクエストを送ると...
http://localhost:55800/api/fizzbuzz?start=10&end=30

こんなのが返ってくるWebAPI
["Buzz","11","Fizz","13","14","FizzBuzz","16","17","Fizz","19","Buzz","Fizz","22","23","Fizz","Buzz","26","Fizz","28","29","FizzBuzz"]
```

C# とASP.NET Core でつくります。

- DI（依存性の注入）でサービスクラスを注入する
- xUnit を使用した自動テスト
- Swagger を使ったAPI仕様書

## 環境構築

ASP.NET Core Web APIのプロジェクトをつくります。
`Visual C#` > `Web` > `.NET Core` > `ASP.NET Core Web アプリケーション` > `API`

ソリューションにXUnitのプロジェクトを追加します。
`Visual C#` > `Web` > `.NET Core` > `xUnitテスト プロジェクト`

```xml:FizzBuzzApi.csproj
<Project Sdk="Microsoft.NET.Sdk.Web">
  <PropertyGroup>
    <TargetFramework>netcoreapp2.0</TargetFramework>
  </PropertyGroup>
  <ItemGroup>
    <Folder Include="wwwroot\" />
  </ItemGroup>
  <ItemGroup>
    <PackageReference Include="Microsoft.AspNetCore.All" Version="2.0.8" />
  </ItemGroup>
  <ItemGroup>
    <DotNetCliToolReference Include="Microsoft.VisualStudio.Web.CodeGeneration.Tools" Version="2.0.4" />
  </ItemGroup>
</Project>
```

```xml:FizzBuzzApiTest.csproj
<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <TargetFramework>netcoreapp2.0</TargetFramework>
    <IsPackable>false</IsPackable>
  </PropertyGroup>
  <ItemGroup>
    <PackageReference Include="Microsoft.NET.Test.Sdk" Version="15.7.0" />
    <PackageReference Include="xunit" Version="2.3.1" />
    <PackageReference Include="xunit.runner.visualstudio" Version="2.3.1" />
    <DotNetCliToolReference Include="dotnet-xunit" Version="2.3.1" />
  </ItemGroup>
</Project>
```

## FizzBuzzサービスの作成とテスト

`FizzBuzzService`を作成します。
あとから`Web API`のコントローラーにDIします。

#### FizzBuzzの仕様

- 1未満の値が渡されると例外をスローする。
- 3で割り切れる数字を渡すと、文字列 `"Fizz"` を返す。
- 5で割り切れる数値を渡すと、文字列 `"Buzz"` を返す。
- 15で割り切れる数値を渡すと、文字列 `"FizzBuzz"` を返す。
- 上記以外の数値が渡されると、数値を文字列に変えて返す。

まず、FizzBuzzApiにインターフェースを作成します。
拡張が強くになる、`Moq`でモックアップが簡単といったメリットがあります。

```csharp:FizzBuzzApi/Interfaces/IFizzBuzz.cs
namespace FizzBuzzApi.Interfaces
{
    public interface IFizzBuzz
    {
        string GetWord(int n);
    }
}
```

インターフェースを実装した`FizzBuzzService`クラスを作成します。
`public class FizzBuzzService : IFizzBuzz { }`まで書き`Ctrl`+`.`を押すと以下のようなスケルトンが自動実装されます。

```csharp:FizzBuzzApi/Services/FizzBuzzService.cs
using FizzBuzzApi.Interfaces;
using System;

namespace FizzBuzzApi.Services
{
    public class FizzBuzzService : IFizzBuzz
    {
        public string GetWord(int n) => throw new NotImplementedException();
    }
}
```

テスト駆動開発では `テストを書く` > `テストをパスする最小限のコードを書く` > `リファクタリングする` を繰り返します。

`FizzBuzzApiTest`の参照に`FizzBuzzApi`を追加します。
`Services`フォルダを作成し、`FizzBuzzServiceTest.cs`を作成します。

過程を省略しますが、以下のようになりました。

```csharp:FizzBuzzApiTest/Services/FizzBuzzServiceTest.cs
using FizzBuzzApi.Services;
using System;
using Xunit;

namespace FizzBuzzApiTest.Services
{
    public class FizzBuzzServiceTest
    {
        [Theory]
        [InlineData(1, "1")]
        [InlineData(2, "2")]
        [InlineData(3, "Fizz")]
        [InlineData(4, "4")]
        [InlineData(5, "Buzz")]
        [InlineData(6, "Fizz")]
        [InlineData(7, "7")]
        [InlineData(10, "Buzz")]
        [InlineData(15, "FizzBuzz")]
        [InlineData(30, "FizzBuzz")]
        public void ReturnsValidString(int n, string word)
        {
            // sut はSystem under testを指す
            var sut = new FizzBuzzService();
            Assert.Equal(word, sut.GetWord(n));
        }

        [Theory]
        [InlineData(0)]
        [InlineData(-1)]
        public void LessThan1_ThrowsException(int n)
        {
            var sut = new FizzBuzzService();
            var ex = Assert.Throws<ArgumentException>(() =>
            {
                sut.GetWord(n);
            });
            Assert.Contains("は1以上としてください", ex.Message);
        }
    }
}
```

```csharp:FizzBuzzApi/Services/FizzBuzzService.cs
using FizzBuzzApi.Interfaces;
using System;

namespace FizzBuzzApi.Services
{
    public class FizzBuzzService : IFizzBuzz
    {
        public string GetWord(int n)
        {
            if (n < 1)
                throw new ArgumentException($"{nameof(n)}は1以上としてください");

            if (n % 15 == 0)
                return "FizzBuzz";
            else if (n % 3 == 0)
                return "Fizz";
            else if (n % 5 == 0)
                return "Buzz";
            else
                return n.ToString();
        }
    }
}
```

テストはすべてパスしています。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/195024/4adb15da-7ecc-ba32-b037-16b0e8aa422a.png)

## `FizzBuzzService` をDI（依存性の注入）する

ASP.NET Core には DI コンテナーがビルトインされています。
`Startup.cs` の `ConfigureServices`メソッドを編集します。
`FizzBuzzService`に依存するクラスのコンストラクターの引数に`FizzBuzzService`のインスタンスが渡されます。

```cs:FizzBuzzApi/Startup.cs
...
        public void ConfigureServices(IServiceCollection services) => 
            services.AddTransient<IFizzBuzz, FizzBuzzService>()
                .AddMvc();
...
```


HTTP要求を受け付けるコントローラーを実装します。

```csharp:FizzBuzzApi/Controllers/FizzBuzzController.cs
using System.Collections.Generic;
using System.Linq;
using FizzBuzzApi.Interfaces;
using Microsoft.AspNetCore.Mvc;

namespace FizzBuzzApi.Controllers
{
    [Produces("application/json")]
    [Route("api/FizzBuzz")]
    public class FizzBuzzController : Controller
    {
        private readonly IFizzBuzz _fizzbuzz;

        // Startup.cs の設定によりコンストラクタの引数に FizzBuzzService が渡される
        public FizzBuzzController(IFizzBuzz fizzbuzz) =>
            _fizzbuzz = fizzbuzz;

        [HttpGet]
        public IEnumerable<string> Get(int from, int to)
        {
            var count = to - from + 1;
            return Enumerable.Range(from, count)
                .Select(n => _fizzbuzz.GetWord(n));
        }

    }
}
```

アプリを起動したらPostmanでリクエストを送って動確します。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/195024/795024b1-7195-9b9e-2b82-3da0978971f6.png)


```json
[
    "Fizz",
    "13",
    "14",
    "FizzBuzz",
    "16",
    "17",
    "Fizz",
    "19",
    "Buzz",
    "Fizz",
    "22",
    "23",
    "Fizz",
    "Buzz",
    "26",
    "Fizz",
    "28",
    "29",
    "FizzBuzz",
    "31",
    "32",
    "Fizz",
    "34",
    "Buzz"
]
```

powershell でも。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/195024/c72eaab5-3b67-975d-8640-1766c953b90d.png)

```powershell
PS C:\Program Files\PowerShell\6.0.2> Invoke-WebRequest -Uri "http://localhost:49261/api/FizzBuzz?from=12&to=35"


StatusCode        : 200
StatusDescription : OK
Content           : ["Fizz","13","14","FizzBuzz","16","17","Fizz","19","Buzz","Fizz","22","23","Fizz","Buzz","26","Fizz
                    ","28","29","FizzBuzz","31","32","Fizz","34","Buzz"]
RawContent        : HTTP/1.1 200 OK
                    Date: Sat, 09 Jun 2018 03:35:52 GMT
                    Transfer-Encoding: chunked
                    Server: Kestrel
                    X-SourceFiles: =?UTF-8?B?QzpcVXNlcnNcc2E1MDBcc291cmNlXHJlcG9zXEZpenpCdXp6QXBpXEZpenpCdXp6QXBpXGFwaV
                    xG...
Headers           : {[Date, System.String[]], [Transfer-Encoding, System.String[]], [Server, System.String[]], [X-Sourc
                    eFiles, System.String[]]...}
Images            : {}
InputFields       : {}
Links             : {}
RawContentLength  : 151
RelationLink      : {}
```

（powershell がいつの間にか Linux や mac 対応のクロスプラットフォームになっていました。PowerShell Core というそうです。変数などはすべて `.NET` のオブジェクトであり、またコードネームがモナドというように関数型プログラミングが可能とモダンです。さすが名前に "power" がついていたり次世代 shell とか呼ばれるだけある。。。）

## Swaggerドキュメントの出力

`NSwag` と `Swashbuckle` を使った方法がありますが、`NSwag` を選択します。
Visual Studio のパッケージマネージャーコンソールで以下のコマンドを入力します。

```powershell:PMC
PS > Install-Package NSwag.AspNetCore
```

`Startup.cs` の `Configure` メソッドに `UseSwaggerUi` を追加します。

```csharp
        public void Configure(IApplicationBuilder app, IHostingEnvironment env)
        {
            if (env.IsDevelopment())
                app.UseDeveloperExceptionPage();

            app.UseSwaggerUi(typeof(Startup).GetTypeInfo().Assembly, settings =>
            {
                settings.GeneratorSettings.DefaultPropertyNameHandling =
                    PropertyNameHandling.CamelCase;
            });

            app.UseMvc();
        }
```

アプリを起動し `http://localhost:<port>/swagger` に移動すると、Swagger UI が表示されます。 
![image.png](https://qiita-image-store.s3.amazonaws.com/0/195024/4fe3d14c-5654-5c94-49dc-0a98d30a8715.png)


`http://localhost:<port>/swagger/v1/swagger.json` に移動すると、Swagger 仕様が表示されます。

```json:swagger.json
{
  "x-generator": "NSwag v11.17.13.0 (NJsonSchema v9.10.50.0 (Newtonsoft.Json v10.0.0.0))",
  "swagger": "2.0",
  "info": {
    "title": "My Title",
    "version": "1.0.0"
  },
  "host": "localhost:49261",
  "schemes": [
    "http"
  ],
  "consumes": [
    "application/json"
  ],
  "produces": [
    "application/json"
  ],
  "paths": {
    "/api/FizzBuzz": {
      "get": {
        "tags": [
          "FizzBuzz"
        ],
        "operationId": "FizzBuzz_Get",
        "parameters": [
          {
            "type": "integer",
            "name": "from",
            "in": "query",
            "required": true,
            "format": "int32",
            "x-nullable": false
          },
          {
            "type": "integer",
            "name": "to",
            "in": "query",
            "required": true,
            "format": "int32",
            "x-nullable": false
          }
        ],
        "responses": {
          "200": {
            "x-nullable": true,
            "description": "",
            "schema": {
              "type": "array",
              "items": {
                "type": "string"
              }
            }
          }
        }
      }
    }
  }
}
```

以下の目標をすべて達成することができました。

- DI（依存性の注入）でサービスクラスを注入する
- xUnit を使用した自動テスト
- Swagger を使ったAPI仕様書