vaguely

和歌山に戻りました。ふらふらと色々なものに手を出す毎日。

【ASP.NET Core】単体テストってみる -導入と Controller のテスト

はじめに

これは C# Advent Calendar 2018 十七日目の穴埋め記事です。

前回 Azure Pipeline で Unity プロジェクトのビルドをしようとしたらちょっと厳しそう (少なくとも現状では)、という結論になったわけですが、まずは Azure Pipeline 力をゲットするやで!ということで、 ASP.NET Core のビルドに挑戦してみることにしました。

で、CI でビルド + マージしていくとなれば、テストが欲しいところ。

ということで、今回は xUnit と Moq を使ったテストに挑戦してみます。

どこからテストするか?というところで迷ったのですが、まずは Controller クラスから試してみることにします。

環境

  • .NET Core ver.2.2.101
  • xUnit ver.2.4.1
  • Moq ver.4.10.1
  • Rider ver.2018.2.3

テスト対象のクラス

過去の記事で書いたコードをもとにこんなクラスにしてみました。

HomeController.cs

using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using AspNetCoreTestSample.FileLoaders;
using AspNetCoreTestSample.Models;
using Microsoft.AspNetCore.Mvc;

namespace AspNetCoreTestSample.Controllers
{
    public class HomeController: Controller
    {
        private readonly ILocalFileLoader _fileLoader;

        public HomeController(ILocalFileLoader fileLoader)
        {
            _fileLoader = fileLoader;
        }
        [Route("")]
        public IActionResult Index()
        {
            ViewData["SampleUsers"] = Enumerable.Range(0, 5)
                .Select(n => "User " + n)
                .ToList();
            return View("/Views/Index.cshtml");
        }

        [Route("/items/update")]
        [HttpPost]
        [Produces("application/json")]
        public async Task< List< string>> ReloadAsync()
        {
            await _fileLoader.ReloadFilePathsAsync();
            return _fileLoader.FilePaths;
        }
    }
}

ILocalFileLoader.cs

using System.Collections.Generic;
using System.Threading.Tasks;

namespace AspNetCoreTestSample.FileLoaders
{
    public interface ILocalFileLoader
    {
        List< string> FilePaths { get; }
        Task ReloadFilePathsAsync();
    }
}

準備

テスト用のプロジェクトを追加する

テスト用にプロジェクト( Unit Test Project )を Type を xUnit にして追加します。

名前は テスト対象のプロジェクト名.Tests が良いらしいです。

プロジェクトが出来上がったら、 NuGet で Moq をテストプロジェクトにインストールします。

テストプロジェクト上で右クリック > Add > Add Reference... で、テスト対象のプロジェクトへの参照を追加します。

なお、この参照設定を行うため、 NuGet でパッケージをインストールする時はどちらか一方にのみインストールする必要があります(重複によるエラーを避けるため)。

NuGet でさらにパッケージをインストール

プロジェクトの作成自体は完了なのですが、実際に下記を書いてみるとエラーになります。

HomeControllerTest.cs

using System;
using AspNetCoreTestSample.Controllers;
using AspNetCoreTestSample.FileLoaders;
using Moq;
using Xunit;

namespace AspNetCoreTestSample.Tests.Controllers
{
    public class HomeControllerTest
    {
        private HomeController _controller;
        
        public HomeControllerTest()
        {
            Mock< ILocalFileLoader> mock = new Mock< ILocalFileLoader>();
            _controller = new HomeController(mock.Object);    
        }
        [Fact]
        public void Sample()
        {
            Assert.Empty("");
        }
    }
}

エラーの内容はこちら。

System.IO.FileNotFoundException : Could not load file or assembly 'Microsoft.AspNetCore.Mvc.ViewFeatures, Version=2.2.0.0, Culture=neutral, PublicKeyToken=example'. 指定されたファイルが見つかりません。

で、これを解決するには、テストプロジェクト側に NuGet で下記を追加インストールする必要があるようです。

テストを書く

では準備も揃ったところで、いくつかテストを書いてみることにします。

HomeControllerTest.cs

using System;
using System.Collections.Generic;
using AspNetCoreTestSample.Controllers;
using AspNetCoreTestSample.FileLoaders;
using Microsoft.AspNetCore.Mvc;
using Moq;
using Xunit;

namespace AspNetCoreTestSample.Tests.Controllers
{
    public class HomeControllerTest: IDisposable
    {
        private readonly HomeController _controller;
        
        public HomeControllerTest()
        {
            // 初期化処理.
            Mock< ILocalFileLoader> mock = new Mock< ILocalFileLoader>();

            // ILocalFileLoader.FilePaths が呼ばれたときに Returns の中身を返す.
            mock.Setup(m => m.FilePaths).Returns(new List< string> {"hello", "world"});

            _controller = new HomeController(mock.Object);    
        }
        public void Dispose()
        {
            // 完了後にアンマネージドリソースの処理したり.
            Console.WriteLine("disposed");
        }
        [Fact]
        public void Index_OpenAndGetType()
        {
            Assert.IsType< ViewResult>(_controller.Index());
        }
        [Fact]
        public async void Reload_GetTwoItems()
        {
            List< string> result = await _controller.ReloadAsync();
            
            Assert.NotNull(result);
            Assert.True(result.Count == 2);
        }
        
    }
}
  • コメントにも書きましたが、アンマネージドリソースの後処理が必要な場合など、 IDispose を継承すると Dispose が呼ばれるようになります。

結果はこちら。

f:id:mslGt:20181220224504j:plain

まだ数が少ないとはいえ、全部成功するのは気持ち良いものです。

Moq について

DI で挿入している依存クラスは、 Moq を使って自動でダミーデータを生成してもらうことができます。

ただ当然ながら Moq で生成したダミーデータのメソッドを呼んでも動作はしないため、その戻り値がテストに必要な場合、 Setup 、 Returns などを利用してダミーのデータを設定することができます。

制限事項として、 Setup で扱うには、そのメソッドが virtual である( override 可能)か、 class ではなく interface のモックを作る必要があります。

おわりに

俺たちの自動テストは始まったばかりだ。。。!

。。。というのは置いといて、 Moq を使って依存する class を自動生成できるのは結構便利ですね :)

まだどこをテストすべきか、といった部分が理解できていなかったり(なので作りながらテストを書いたり消したりしている)、 View や Model など他の機能のテストの書き方も分かっていなかったりするので、まぁ、まだこれからですがぼちぼち進めていきたいと思います。

参照

xUnit

Moq