C#
テスト
DependencyInjection
xUnit
ASP.NET_Core

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テスト プロジェクト

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>
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でモックアップが簡単といったメリットがあります。

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

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

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を作成します。

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

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);
        }
    }
}
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

FizzBuzzService をDI(依存性の注入)する

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

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

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

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

[
    "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

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ドキュメントの出力

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

PMC
PS > Install-Package NSwag.AspNetCore

Startup.csConfigure メソッドに UseSwaggerUi を追加します。

        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

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

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仕様書