ひでぼ~blog

C#ときどきゲーム

WebApplicationFactoryを使ってASP .NET Core WebAPIでE2Eテストを試す

実行環境

準備

ASP .NET Core WebAPIのプロジェクトとユニットテストのプロジェクトを作成し、ユニットテストのプロジェクトからWebAPIのプロジェクトを参照するように設定しておきます。 また、Program.csはトップレベステートメントで書かれていてclassの記述が省略されています。classのアクセス修飾子がpublicになっていないので、public partial class Program{}という記述を追加します。

var builder = WebApplication.CreateBuilder(args);

// Add services to the container.

builder.Services.AddControllers();
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();

var app = builder.Build();

// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
    app.UseSwagger();
    app.UseSwaggerUI();
}

app.UseHttpsRedirection();

app.UseAuthorization();

app.MapControllers();

app.Run();

// ここを追加
public partial class Program{   }

コントローラはデフォルトで作られるWeatherForecastControllerをそのまま使います。

public class WeatherForecastController : ControllerBase
{
    private static readonly string[] Summaries = new[]
    {
        "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
    };

    private readonly ILogger<WeatherForecastController> _logger;

    public WeatherForecastController(ILogger<WeatherForecastController> logger)
    {
        _logger = logger;
    }

    [HttpGet(Name = "GetWeatherForecast")]
    public IEnumerable<WeatherForecast> Get()
    {
        return Enumerable.Range(1, 5).Select(index => new WeatherForecast
        {
            Date = DateOnly.FromDateTime(DateTime.Now.AddDays(index)),
            TemperatureC = Random.Shared.Next(-20, 55),
            Summary = Summaries[Random.Shared.Next(Summaries.Length)]
        })
        .ToArray();
    }
}

テストを書く

テストフレームワークはxUnitを使います。コンストラクタで注入されるWebApplicationFactoryを使っていきます。

public class WeatherForecastControllerTest(WebApplicationFactory<Program> factory) : IClassFixture<WebApplicationFactory<Program>>
{
    private readonly WebApplicationFactory<Program> _factory = factory;

    [Fact]
    public async Task GetTestAsync()
    {
        var client = _factory.CreateClient();

        var response = await client.GetAsync("/WeatherForecast");

        Assert.True(response.IsSuccessStatusCode);
    }
}

テストを実行して成功することが確認できました。

参考

learn.microsoft.com

Dockerを使ったパルワールド専用サーバの構築方法

パルワールドの専用サーバを建ててみたかったのでDockerコンテナを使ってやってみました。

実行環境

専用サーバの準備

docker composeで簡単に専用サーバのコンテナを作ってくれるRepositoryがあったのでありがたく使わせてもらいます。

github.com

git clone後にプロジェクトルートで実行します。

docker compose up -d --build

GUIからコンテナが動いてることが分かります。

専用サーバに接続してみる

パルワールドのクライアントを起動して専用サーバに接続してみます。localhostに専用サーバを建てたので127.0.0.1:8211に接続します。 パスワードはデフォルトでworldofpalsになっています。

入ることができました。

パスワードやポートなどの設定は/palworld/Pal/Saved/Config/LinuxServer/PalWorldSettings.iniにあるので適宜変えていく感じになります。

展望

パルワールドはRCONを使ったコマンド実行に対応しているので、サーバの外からコマンドを実行することができます。 C#からのコマンド実行を試してみるのも面白そうです。

参考

zenn.dev

検証属性付きのrecordからswagger.jsonを生成するときの悩み

ASP.NET Coreでリクエストパラメータにrecordを使った場合に出力されるswagger.jsonにRequired等の属性が反映されない問題があったのでまとめました。 問題になっているのはだいたい次のIssueと同じ内容です。

github.com

最近はエンドポイントのリクエストパラメータにrecordを使うことが多く、次のようなパラメータを作ってASP.NET Coreのエンドポイントで受け取っています。

public record TodoCreateParameters([Required] string Description, [Required] DateTime DateTime)

これを次のようなアクションで受け取ります。

[HttpPost]
public IActionResult CreateTodo([FromBody] TodoCreateParameters parameters)
{
    // 永続化層への登録処理など

    return Created();
}

これでデバッグ実行してエンドポイントを叩いてみると、パラメータがnullのときにバリデーションが効いて400BadRequestが返りうまくいっているように見えるのですが、 swagger.jsonを確認してみると、

  "components": {
    "schemas": {
      "TodoCreateParameters": {
        "type": "object",
        "properties": {
          "description": {
            "type": "string",
            "nullable": true
          },
          "dateTime": {
            "type": "string",
            "format": "date-time"
          }
        },
        "additionalProperties": false
      }
    }
  }

TodoCreateParametersのdescriptionとdateTimeがrequiredになっていません。

なので属性のAttributeTargetsをpropertyと明示するようにしました。

public record TodoCreateParameters([property: Required] string Description, [property: Required] DateTime DateTime)

すると生成されるswagger.jsonはrequiredがつくようになりました。

  "components": {
    "schemas": {
      "TodoCreateParameters": {
        "required": [
          "dateTime",
          "description"
        ],
        "type": "object",
        "properties": {
          "description": {
            "minLength": 1,
            "type": "string"
          },
          "dateTime": {
            "type": "string",
            "format": "date-time"
          }
        },
        "additionalProperties": false
      }
    }
  }

ただ、ここでエンドポイントを叩いてみるとエラーになります。

System.InvalidOperationException: Record type 'PrimaryCtorValidationSample.TodoCreateParameters' has validation metadata defined on property 'DateTime' that will be ignored. 'DateTime' is a parameter in the record primary constructor and validation metadata must be associated with the constructor parameter.

プライマリコンストラクタの引数はコンストラクタの引数とプロパティを兼ねているので、どっちにつける属性なのかハッキリしてほしいみたいな内容だと思います。 エラーメッセージに従って、[property: Required]を[param: Required]に変えてみました。すると今度は実行時エラーは出なくなりましたがswagger.jsonにrequiredが反映されなくなりました…。

良い解決策が見つからず妥協して次のようにrecordを作りました。

public record TodoCreateParameters
{
    public TodoCreateParameters(string description, DateTime dateTime)
    {
        Description = description;
        DateTime = dateTime;
    }

    [Required]
    public string Description { get; init; }

    [Required]
    public DateTime DateTime { get; init; }
}

プロパティとコンストラクタが簡潔に書けるrecordの良いところが無くなってますがこれでやりたいことはできるようになりました。 ただこれだとReSharperがプライマリーコンストラクタを使うように波線を出して提案してくるのが少しウザいです…。

VS2022のhttpファイルで環境毎のパラメータを用意する

実行環境

  • VS2022 17.8.3
  • .NET 8

httpファイルを用意する

適当なhttpファイルを用意します。HostAddressとParameterは後述の別ファイルで定義します。

GET {{HostAddress}}?q={{Parameter}}

httpenv.jsonを用意する

httpenv.jsonを作成してhttpファイルと同じ階層か上位の階層に置きます。 develop, staging, productionの3つの環境を想定してそれぞれのHostAddressとParameterを設定しました。

{
  "develop": {
    "HostAddress": "https://example.com",
    "Parameter": "d"
  },
  "staging": {
    "HostAddress": "https://example.com",
    "Parameter": "s"
  },
  "production": {
    "HostAddress": "https://example.com",
    "Parameter": "p"
  }
}

httpファイルを開くと、エディターの左上で環境が選択できるようになります。

httpenv.json.userを用意する

ソース管理したくないような個人で使うパラメータはhttpenv.json.userを使います。httpenv.jsonと同じ階層に配置します。

{
  "develop": {
    "HostAddress": "https://example.com",
    "Parameter": "user-d"
  },
  "staging": {
    "HostAddress": "https://example.com",
    "Parameter": "user-s"
  },
  "production": {
    "HostAddress": "https://example.com",
    "Parameter": "user-p"
  }
}

パラメータの優先順位

  1. httpファイル内で宣言された変数
  2. httpenv.json.userファイルで宣言された変数
  3. httpenv.jsonファイルで宣言された変数

試しにhttpenv.json.userとhttpenv.jsonがある状態でリクエストを投げてみました。

httpenv.json.userの値が使われていました。

参考

devblogs.microsoft.com

FakeTimeProviderを試す

.NET 8からDateTimeに依存した処理のテストに役立つTimeProviderとFakeTimeProviderという仕組みが追加されたので試していきます。

実行環境

  • VS2022 17.8.1
  • .NET 8

準備

NuGetパッケージをインストールします。これでFakeTimeProviderが使えるようになります。 www.nuget.org

現在時刻をセットする

SetUtcNowでTimeProviderが任意の現在時刻を返すようにできます。

var timeProvider = new FakeTimeProvider();
timeProvider.SetUtcNow(new DateTimeOffset(new DateTime(2023, 11, 30)));
Console.WriteLine(timeProvider.GetLocalNow());
2023/11/30 0:00:00 +09:00

GetUtcNowが返す型はDateTimeではなくDateTimeOffsetになっています。

TimeZoneをセットする

SetLocalTimeZoneで任意のタイムゾーンをセットすることができます。

var timeProvider = new FakeTimeProvider();
timeProvider.SetUtcNow(new DateTimeOffset(new DateTime(2023, 11, 30)));
timeProvider.SetLocalTimeZone(TimeZoneInfo.FindSystemTimeZoneById("Hawaiian Standard Time"));
Console.WriteLine(timeProvider.GetLocalNow());
2023/11/29 14:00:00 -10:00

現在時刻を進める

Advanceで現在時刻を任意の時間分進めたり戻したりできます。

var timeProvider = new FakeTimeProvider();
timeProvider.SetUtcNow(new DateTimeOffset(new DateTime(2023, 11, 30)));
Console.WriteLine(timeProvider.GetLocalNow());

timeProvider.Advance(TimeSpan.FromMinutes(3));

Console.WriteLine(timeProvider.GetLocalNow());
2023/11/30 0:00:00 +09:00
2023/11/30 0:03:00 +09:00

現在時刻を取得するたびに現在時刻を進める

AutoAdvanceAmountに任意のTimeSpanをセットすることで、GetLocalNowで時刻を取得するたびにセットしたTimeSpan分時刻が進むようになります。

var timeProvider = new FakeTimeProvider();
timeProvider.SetUtcNow(new DateTimeOffset(new DateTime(2023, 11, 30)));
timeProvider.AutoAdvanceAmount = TimeSpan.FromSeconds(3);

for (int i = 0; i < 10; i++)
{
    Console.WriteLine(timeProvider.GetLocalNow());
}
2023/11/30 0:00:00 +09:00
2023/11/30 0:00:03 +09:00
2023/11/30 0:00:06 +09:00
2023/11/30 0:00:09 +09:00
2023/11/30 0:00:12 +09:00
2023/11/30 0:00:15 +09:00
2023/11/30 0:00:18 +09:00
2023/11/30 0:00:21 +09:00
2023/11/30 0:00:24 +09:00
2023/11/30 0:00:27 +09:00

参考

devblogs.microsoft.com

コードを書くためにサブスクライブしているもの

C#ASP.NET Coreを使ったWeb開発をメインでやっている私がコードを書くためにサブスクライブしているものをあげていきます。

ReSharper

言わずと知れたVisual Studio拡張機能です。本家よりも強力なIntelliSence、括弧の自動補完などなどいろんな機能に助けられています。 特に最高なのが、リファクタリングの機能で、例えばちょっといまいちなコードを書いたときに最新の言語機能を使っていい感じにリファクタリングしてくれたりします。

複数のif文をswitch式にリファクタリングしてくれる

最近はAIアシスタントというAIによるサポート機能が追加されて、まだベータ版なのですが結構気に入っています。 メソッドの命名に迷ったときに、コードの実装をチャットに張り付けて「このメソッドの命名迷ってるんだけど何か良い名前ない?」って感じで聞いたりしてます。

www.jetbrains.com

Git Fork

コミットやチェックアウトなど、Gitの基本的な操作はメインのIDEであるVisual StudioGUIで事足りるのですが、ちょっと複雑なブランチのマージやリベースを行うときに微妙に手の届かないかゆいところがあってGit ForkというGitクライアントを使っています。 Git Forkは他のGitクライアントと比べてUIがカッコいいです。コミットグラフの線の描画が滑らかで綺麗なので気に入っています。

Git Forkのカッコいいコミットグラフ

git-fork.com

LINQPad

C#のREPLです。 ちょっとしたC#のコードの動作確認をしたいときなどに使っています。 前はVisual StudioC#インタラクティブを使っていましたが、複数行にわたるコードを書くときはちょっと物足りないのでLINQPadを使っています。 無料版もありますがIntelliSenceが使えなかったりするので有料版を使っています。

変数の中身をDumpして確認している様子

www.linqpad.net

Swashbuckle.AspNetCore.Cliでswagger.jsonを出力する

CI/CDでswagger.jsonを出力したかったのでやってみました。

実行環境

  • VS2022 17.7.4
  • .NET 7
  • Swashbuckle.AspNetCore.Cli 6.5.0

Swashbuckle.AspNetCore.Cliをインストールする

プロジェクトのルートで.NET ツールをインストールします。

dotnet new tool-manifest
dotnet tool install Swashbuckle.AspNetCore.Cli

swagger.jsonを出力する

あらかじめASP.NET Core Web APIのプロジェクトを作成しておきReleaseビルドを作成しておきます。 コンソールでプロジェクトのルートに移動して次のコマンドを実行します。

dotnet swagger tofile --output swagger.json SwaggerGenSample/bin/Release/net7.0/SwaggerGenSample.dll v1

引数に出力パス(swagger.json)とアセンブリのパスとStartupで設定したswagger doc(v1)を指定します。

こんな感じでメッセージが出ていればswagger.jsonが出力されてます。

PS C:\Projects\SwaggerGenSample> dotnet swagger tofile --output swagger.json SwaggerGenSample/bin/Release/net7.0/SwaggerGenSample.dll v1
Swagger JSON/YAML successfully written to C:\Projects\SwaggerGenSample\swagger.json