インドカレーファンクラブ

パソコン、カメラ

【.NET5】【xUnit】 MVCのControllerのテストのちょっとした例(ModelStateの状態・明示的な404のテスト)

(色々書いたけど、最後参考に載せているMS Docsに全部書いてある気がしてきた。が、目を背ける)

xUnitとMoqを使う

例えばこんな感じのControllerをテストしたい時がある

public class SampleController : Controller
{
    private readonly ISampleService service;

    public SampleController(ISampleService service)
        => this.service = service;

    [HttpGet]
    public IActionResult Show(SampleParamModel param)
    {
        // paramへのモデルバインド時の検証でエラーがあったら404!
        if (!ModelState.IsValid) { return NotFound(); }
        
        SampleViewModel model = service.GetShowViewModel(param);
        if (model is null) { return NotFound(); }
        return View(model);
    }
}

正常系

正常系、つまるところViewにSampleViewModelのインスタンスを渡せて、それをreturnする場合のテストはそんな悩まないと思う

けど一応書いてみる

[Fact]
public void ShowTest_正常系()
{
    // 必要に応じてもっと真面目にインスタンスをつくる
    var param = new SampleParamModel();

    // MoqでServiceをDIして, Showアクションで呼んでいるメソッドを組み立てる
    var serviceMock = new Mock<IUserService>();
    serviceMock
        .Setup(service => service.GetShowViewModel(param))
        // ここも必要に応じて真面目に
        .Returns(new ShowViewModel());

    // ここからテスト
    var controller = new SampleController(serviceMock.Object);
    var actionResult = controller.Show(param);

    // Assert
    var viewResult = Assert.IsType<ViewResult>(actionResult);
    var model = Assert.IsType<ShowViewModel>(viewResult.Model);

    // シンプルな例すぎて何をAssertすればいいのかわからない
    // あとなんか適当にmodelのフィールドあたりを...
}

準正常系

こっちの内容を書きたかった!

// paramへのモデルバインド時の検証でエラーがあったら404!
if (!ModelState.IsValid) { return NotFound(); }

コレに落ちるパターンを書くと、こうなる

[Fact]
public void ShowTest_準正常系_パラメータエラー()
{
    // 今回はServiceの処理を呼ぶ前に落とすからSetupはしない
    var serviceMock = new Mock<ISampleService>();

    var controller = new SampleController(serviceMock.Object);

    // ここが肝
    controller.ModelState.AddModelError(nameof(SampleParamModel.Name), "some error");

    // ModelStateでエラーを入れ込むのでparamのインスタンスに特に意味はない
    var param = new SampleParamModel();
    var actionResult = controller.Show(param);

    // Assert
    // return NotFound()したものはこんな感じに引っ掛ける
    Assert.IsType<NotFoundResult>(actionResult);
}

ModelStateに直接エラーを仕込むのがポイント!

一応ちょっと悩んだところとして、例えばこんなことをしても意味無し

var controller = new SampleController(serviceMock.Object);
var param = new SampleParamModel() { Name = ""}; // Nameに[Required]がついているとする
var actionResult = controller.Show(param);
// この時点でcontroller.ModelStateはValidになっている

ModelStateは飛んできたアクセスルーティングの指示通りControllerのActionに振ってモデルバインドさせたタイミングでチェックされているようで、上記のように単純にインスタンスを渡すだけではダメみたい
(真面目に内部実装を追いたいけど今回はやめた)

一応以下のようにControllerに渡す前のparamを明示的にValidatorにかけてみたけどコレも意味無し
(ModelStateはparamじゃなくてControllerのインスタンスにくっついてるんだからそりゃそうだ) Validator.TryValidateObject(param, new ValidationContext(param), new List<ValidationResult>(), true);

参考:

https://docs.microsoft.com/ja-jp/aspnet/core/mvc/controllers/testing?view=aspnetcore-5.0

https://stackoverflow.com/questions/58855965/unit-test-aspnetcore-controller-check-httpstatuscode-with-actionresultt-result

https://stackoverflow.com/questions/22561834/asp-net-mvc-controller-post-method-unit-test-modelstate-isvalid-always-true