LoginSignup
3
5

More than 3 years have passed since last update.

Asp.net core 2.2 のチュートリアルを単体テストのサンプルの構成で試してCentos7.6 + Sql Serverで動かしたメモ

Last updated at Posted at 2019-05-19

概要

単体テストのサンプルMvcチュートリアルのフォルダ構成が異なっていたので、単体テストの構成でチュートリアルを試してみる。
ついでにCentOS上で動かしてみる。

環境の準備については以下。
sql serverの準備
visual studio + dotnet core 2.2

環境

  • Windows 10
  • Visual Studio 2017 Community
  • Vagrant 2.2.4
  • virtualbox 6.0.6r130049

ディレクトリ構成

[vagrant@localhost vagrant]$ tree .
.
├── MvcMovie
│   ├── MvcMovie ... Asp.net プロジェクト
│   │   ├── Areas ... 以下、認証のスキャフォールディングで追加されるファイル
│   │   │   └── Identity
│   │   │       ├── Data
│   │   │       │   └── MvcMovieIdentityContext.cs
│   │   │       ├── IdentityHostingStartup.cs
│   │   │       └── Pages
│   │   ├── Controllers ... コントローラ
│   │   │   ├── HelloWorldController.cs
│   │   │   ├── HomeController.cs
│   │   │   └── MoviesController.cs
│   │   ├── Core ... モデルとインターフェース
│   │   │   ├── Interfaces
│   │   │   │   ├── IMovieRepository.cs
│   │   │   │   └── IRepository.cs
│   │   │   └── Models
│   │   │       ├── ErrorViewModel.cs
│   │   │       └── Movie.cs
│   │   ├── Data
│   │   │   └── MvcMovieContext.cs
│   │   ├── Infrastructure ... リポジトリ
│   │   │   ├── MovieRepository.cs
│   │   │   └── Repository.cs
│   │   ├── Migrations ... マイグレーション
│   │   ├── MvcMovie.csproj
│   │   ├── MvcMovie.csproj.user
│   │   ├── Program.cs
│   │   ├── Properties ... 発行で使用
│   │   │   ├── PublishProfiles
│   │   │   │   ├── FolderProfile.pubxml
│   │   │   │   └── FolderProfile.pubxml.user
│   │   │   └── launchSettings.json
│   │   ├── ScaffoldingReadme.txt
│   │   ├── Startup.cs
│   │   ├── Views
│   │   │   ├── HelloWorld
│   │   │   │   └── Index.cshtml
│   │   │   ├── Home
│   │   │   │   ├── Index.cshtml
│   │   │   │   └── Privacy.cshtml
│   │   │   ├── Movies
│   │   │   │   ├── Create.cshtml
│   │   │   │   ├── Delete.cshtml
│   │   │   │   ├── Details.cshtml
│   │   │   │   ├── Edit.cshtml
│   │   │   │   └── Index.cshtml
│   │   │   ├── Shared
│   │   │   │   ├── Error.cshtml
│   │   │   │   ├── _CookieConsentPartial.cshtml
│   │   │   │   ├── _Layout.cshtml
│   │   │   │   ├── _LoginPartial.cshtml
│   │   │   │   └── _ValidationScriptsPartial.cshtml
│   │   │   ├── _ViewImports.cshtml
│   │   │   └── _ViewStart.cshtml
│   │   ├── appsettings.Development.json
│   │   ├── appsettings.json
│   │   ├── bin
│   │   │   ├── Debug
│   │   │   └── Release
│   │   │       └── netcoreapp2.2
│   │   │           └── publish ... 発行の出力先
│   │   ├── obj
│   ├── MvcMovie.Test ... テストプロジェクト
│   │   ├── Controllers
│   │   │   └── MovieControllerTest.cs
│   │   ├── MvcMovie.Test.csproj
│   │   ├── UnitTest1.cs
│   │   ├── bin
│   │   └── obj
│   └── MvcMovie.sln
├── Vagrantfile
├── bin
│   └── up.sh
├── docker
│   ├── docker-compose.yml
│   └── nginx
│       ├── Dockerfile
│       └── default.conf
└── provision
    ├── bash
    │   └── install_ansible.sh
    ├── docker
    │   └── docker-compose.yml
    └── playbooks

実践

ソリューションの作成

作成時点

コントローラの追加

コントローラ追加
コントローラ追加時点ソース書き換え時点ソース

ビューの追加

ビューの追加
* 日本語のVisual Studioでは[view]ではなく[ビュー]を検索する

モデルの追加

単体テストのチュートリアルでは、ModelはCoreの下にあるので、フォルダと名前空間を変更する。
Core/Models/Movie.csを作成。
モデルの追加
フォルダ変更スキャフォールディング追加後ソースエラー発生中ソース

このままだとDBエラーなのでマイグレーション。

PM> Add-Migration Initial
PM> Update-Database

2019-05-19.png
DBマイグレーション後ソース

リポジトリ作成

単体テストのチュートリアルでは、リポジトリパターンを使っているので、スキャフォールディングで作られたソースを修正する。
インターフェースはCore/Interfacesに、リポジトリはInfrastructureに作成する。
リポジトリパターン追加時ソース

インターフェース作成

Core/Interfaces/IRepository.cs
using System.Collections.Generic;
using System.Threading.Tasks;

namespace MvcMovie.Core.Interfaces
{
    public interface IRepository<Type, Key>
    {
        Task<Type> GetByIdAsync(Key key);
        Task<List<Type>> ListAsync();
        Task AddAsync(Type type);
        Task UpdateAsync(Type type);
        Task DeleteAsync(Type type);
        bool Exists(Key key);
    }
}
Core/Interfaces/IMovieRepository.cs
using MvcMovie.Core.Models;

namespace MvcMovie.Core.Interfaces
{
    public interface IMovieRepository : IRepository<Movie, int> { }
}

リポジトリ作成

Core/Infrastructure/Repository.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using MvcMovie.Core.Interfaces;
using System.Threading;
using System.Linq.Expressions;

namespace MvcMovie.Infrastructure
{
    public abstract class Repository<Type, Key> : IRepository<Type, Key>
        where Type : class
    {
        private readonly DbContext _dbContext;

        public Repository(DbContext dbContext)
        {
            _dbContext = dbContext;
        }

        public abstract Task<Type> GetByIdAsync(Key key);
        protected Task<Type> GetByIdAsync(Expression< Func<Type, bool>> func)
        {
            return _dbContext.Set<Type>().FirstOrDefaultAsync(func, default(CancellationToken));
        }

        public Task<List<Type>> ListAsync()
        {
            var movies = from m in _dbContext.Set<Type>()
                         select m;

            return movies.ToListAsync();

        }

        public Task AddAsync(Type type)
        {
            _dbContext.Add(type);
            return _dbContext.SaveChangesAsync();
        }

        public Task UpdateAsync(Type type)
        {
            _dbContext.Update(type);
            return _dbContext.SaveChangesAsync();
        }

        public Task DeleteAsync(Type type)
        {
            _dbContext.Set<Type>().Remove(type);
            return _dbContext.SaveChangesAsync();
        }
        public abstract bool Exists(Key key);
        protected bool Exists(Predicate<Type> func)
        {
            return _dbContext.Set<Type>().ToList().Exists(func);
        }
    }
}
Infrastructure/MovieRepository.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using MvcMovie.Core.Interfaces;
using MvcMovie.Core.Models;
using MvcMovie.Models;

namespace MvcMovie.Infrastructure
{
    public class MovieRepository :  Repository<Movie, int>, IMovieRepository
    {
        private readonly MvcMovieContext _dbContext;

        public MovieRepository(MvcMovieContext dbContext):base(dbContext)
        {
            _dbContext = dbContext;
        }

        public override Task<Movie> GetByIdAsync(int id)
        {
            return this.GetByIdAsync(m => m.Id == id);
        }

        public override bool Exists(int id)
        {
            return this.Exists(e => e.Id == id);
        }
    }
}

コントローラ修正

Controllers/MoviesController.cs
+using System.Threading.Tasks;
+using MvcMovie.Core.Interfaces;

 namespace MvcMovie.Controllers
 {
     public class MoviesController : Controller
     {
-        private readonly MvcMovieContext _context;
+        private readonly IMovieRepository _movieRepository;

-        public MoviesController(MvcMovieContext context)
+        public MoviesController(IMovieRepository context)
         {
-            _context = context;
+            _movieRepository = context;
         }

+
         // GET: Movies
         public async Task<IActionResult> Index()
         {
-            return View(await _context.Movie.ToListAsync());
+            return View(await _movieRepository.ListAsync());
         }

         // GET: Movies/Details/5
@@ -33,8 +30,7 @@ namespace MvcMovie.Controllers
                 return NotFound();
             }

-            var movie = await _context.Movie
-                .FirstOrDefaultAsync(m => m.Id == id);
+            var movie = await _movieRepository.GetByIdAsync((int)id);
             if (movie == null)
             {
                 return NotFound();
@@ -58,8 +54,7 @@ namespace MvcMovie.Controllers
         {
             if (ModelState.IsValid)
             {
-                _context.Add(movie);
-                await _context.SaveChangesAsync();
+                await _movieRepository.AddAsync(movie);
                 return RedirectToAction(nameof(Index));
             }
             return View(movie);
@@ -73,7 +68,7 @@ namespace MvcMovie.Controllers
                 return NotFound();
             }

-            var movie = await _context.Movie.FindAsync(id);
+            var movie = await _movieRepository.GetByIdAsync((int)id);
             if (movie == null)
             {
                 return NotFound();
@@ -97,12 +92,11 @@ namespace MvcMovie.Controllers
             {
                 try
                 {
-                    _context.Update(movie);
-                    await _context.SaveChangesAsync();
+                    await _movieRepository.UpdateAsync(movie);
                 }
                 catch (DbUpdateConcurrencyException)
                 {
-                    if (!MovieExists(movie.Id))
+                    if (!_movieRepository.Exists(movie.Id))
                     {
                         return NotFound();
                     }
@@ -124,8 +118,8 @@ namespace MvcMovie.Controllers
                 return NotFound();
             }

-            var movie = await _context.Movie
-                .FirstOrDefaultAsync(m => m.Id == id);
+            var movie = await _movieRepository.GetByIdAsync((int)id);
+
             if (movie == null)
             {
                 return NotFound();
@@ -139,15 +133,9 @@ namespace MvcMovie.Controllers
         [ValidateAntiForgeryToken]
         public async Task<IActionResult> DeleteConfirmed(int id)
         {
-            var movie = await _context.Movie.FindAsync(id);
-            _context.Movie.Remove(movie);
-            await _context.SaveChangesAsync();
+            var movie = await _movieRepository.GetByIdAsync((int)id);
+            await _movieRepository.DeleteAsync(movie);
             return RedirectToAction(nameof(Index));
         }
-
-        private bool MovieExists(int id)
-        {
-            return _context.Movie.Any(e => e.Id == id);
-        }

リポジトリを依存性注入(DI)

依存性注入 (Dependency Injection) の設定をする。

Startup.cs
+using MvcMovie.Core.Interfaces;
+using MvcMovie.Infrastructure;
// 省略
        public void ConfigureServices(IServiceCollection services)
        {
            services.Configure<CookiePolicyOptions>(options =>
            {
                // This lambda determines whether user consent for non-essential cookies is needed for a given request.
                options.CheckConsentNeeded = context => true;
                options.MinimumSameSitePolicy = SameSiteMode.None;
            });


            services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2);

            services.AddDbContext<MvcMovieContext>(options =>
                    options.UseSqlServer(Configuration.GetConnectionString("MvcMovieContext")));

+            services.AddScoped<IMovieRepository, MovieRepository>();
        }

単体テストのチュートリアル

小さくわける命名規則ほど今回は分割していない。この命名規則を使うなら、src/MvcMovieとtests/UnitTests/MvcMovie.UnitTestのようになるか。今回はMvcMovie.Testで作成する。

テストプロジェクトの追加

以下を参考に、テストプロジェクトを追加する

テストプロジェクト追加時ソース

テストの追加

  • 以下のようなソースを作成する
  • この時点だと依存関係が解決されていないため、依存関係を解決してやる

コントローラのテスト追加時ソース

Controllers/MovieControllerTest.cs
using System.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Moq;
using MvcMovie.Controllers;
using MvcMovie.Core.Interfaces;
using MvcMovie.Core.Models;
using Xunit;
using System.Linq;

namespace MvcMovie.Test.Controllers
{
    public class MovieControllerTest
    {
        [Fact]
        public async Task Index_Movieが1つ入ったモデルをもったViewResultが返ること()
        {
            // Arrange
            var mockRepo = new Mock<IMovieRepository>();
            var movies = new List<Movie> { new Movie { Id = 1, Title = "test", Genre = "Test", Price = 10 } };
            mockRepo.Setup(repo => repo.ListAsync(""))
                .ReturnsAsync(movies);

            MoviesController controller = new MoviesController(mockRepo.Object);

            // Act
            var result = await controller.Index("");

            // Assert
            var viewResult = Assert.IsType<ViewResult>(result);
            var model = Assert.IsAssignableFrom<IEnumerable<Movie>>(
                viewResult.ViewData.Model);
            Assert.Equal(1, model.Count());
        }

    }
}

MvcMovieの参照の追加

プロジェクトを右クリックし、[追加]>[参照]>[プロジェクト]>[MvcMovie]

choose_参照.gif

Moqの参照の追加

リポジトリとコントローラを切り離すためのモックを作るライブラリを追加する。
プロジェクトを右クリックし、[追加]>[Nugetパッケージの管理]。
Moqで検索。インストール。

choose_参照_moq.gif

Microsoft.AspNetCore.Mvc.Abstractionsの参照の追加

プロジェクトを右クリックし、[追加]>[Nugetパッケージの管理]。
Microsoft.AspNetCore.Mvc.Abstractionsで検索。インストール。
2019-05-19 (2).png

Microsoft.AspNetCore.Mvc.ViewFeaturesの参照の追加

プロジェクトを右クリックし、[追加]>[Nugetパッケージの管理]。
Microsoft.AspNetCore.Mvc.Abstractionsで検索。インストール。

テストの実行

  • [テスト]>[実行]>[すべてのテスト]
  • テストエクスプローラーを確認 2019-05-19 (3).png

環境の準備については以下。
sql serverの準備

認証

認証のスキャフォールディング

認証のスキャフォールディング

  • ログイン・ログアウト・登録の機能を追加。
  • 同じ名前のContextは付けられないので別の名前のcontextを付ける。今回はMvcMovieIdentityContext

Identityの追加.gif

認証スキャフォールディング後ソース

承認の有効化

--- a/tutorial/lesson/dotnet/tutorial22/MvcMovie/MvcMovie/Startup.cs
+++ b/tutorial/lesson/dotnet/tutorial22/MvcMovie/MvcMovie/Startup.cs
@@ -55,6 +55,7 @@ namespace MvcMovie

             app.UseHttpsRedirection();
             app.UseStaticFiles();
+            app.UseAuthentication();
             app.UseCookiePolicy();

             app.UseMvc(routes =>
diff --git a/tutorial/lesson/dotnet/tutorial22/MvcMovie/MvcMovie/Views/Shared/_Layout.cshtml b/tutorial/lesson/dotnet/tutorial22/MvcMovie/MvcMovie/Views/Shared/_Layout.cshtml
index c9d790b..9a13c2c 100644
--- a/tutorial/lesson/dotnet/tutorial22/MvcMovie/MvcMovie/Views/Shared/_Layout.cshtml
+++ b/tutorial/lesson/dotnet/tutorial22/MvcMovie/MvcMovie/Views/Shared/_Layout.cshtml
@@ -35,6 +35,7 @@
                             <a class="nav-link text-dark" asp-area="" asp-controller="Home" asp-action="Privacy">Privacy</a>
                         </li>
                     </ul>
+                    <partial name="_LoginPartial" />
                 </div>
             </div>
         </nav>

承認有効化後ソース

DBの更新

チュートリアル通りにすると、すでにDbコンテキストがあるためエラーとなる。

PM> Update-Database
More than one DbContext was found. Specify which one to use. Use the '-Context' parameter for PowerShell commands and the '--context' parameter for dotnet commands.

以下のように具体的に指定する。

PM> Add-Migration CreateIdentitySchema -Context MvcMovieIdentityContext
PM> Update-Database -Context MvcMovieIdentityContext

データベースへの接続

データベース準備

ここまでローカルでやってきたが、SqlServerで試してみる。
sql serverの準備
で準備したものに1つ追加する。

D
- name: Identity用DB
  shell: cd /vagrant/provision/docker && /usr/local/bin/docker-compose exec -T {{ docker_container_name}} /opt/mssql-tools/bin/sqlcmd -S localhost -U {{test_user_name}} -P '{{ test_user_pass }}' -Q "IF DB_ID (N'TestIdentityDB') IS NULL create database [TestIdentityDB] collate Japanese_CS_AS" 
  register: result
  changed_when: result.rc != 0
vagrant up
or すでに仮想環境を作っていた場合、
vagrant provision

接続確認

[表示]>[Sql Server オブジェクトエクスプローラー]をクリック

2019-05-19 (6).png

SQL Serverを右クリックして、[SQL Serverの追加] を選択

  • IP: 192.168.50.11
  • SqlServer認証
  • ユーザ: test_db_user
  • パスワード: !test_password001 2019-05-19 (8).png
使用するDBの確認
  • TestDB, TestIdentityDBがあることを確認する。
  • データベースをクリックし、表示されるプロパティから接続文字列を確認

2019-05-19 (9).png

接続の更新

SqlServerを使用に切り替えたソース ... ※ 認証のマイグレーションをこの時点では忘れている

appsettings.json
{
  "Logging": {
    "LogLevel": {
      "Default": "Warning"
    }
  },
  "AllowedHosts": "*",
    "ConnectionStrings": {
-    "MvcMovieContext": "Server=(localdb)\\mssqllocaldb;Database=MvcMovieContext-24336131-8a89-4ef3-bfde-7645e79572d5;Trusted_Connection=True;MultipleActiveResultSets=true",
-    "MvcMovieIdentityContextConnection": "Server=(localdb)\\mssqllocaldb;Database=MvcMovie;Trusted_Connection=True;MultipleActiveResultSets=true"
+        "MvcMovieContext": "Data Source=192.168.50.11;Initial Catalog=TestDB;User ID=test_db_user;Password=!test_password001;Connect Timeout=30;Encrypt=False;TrustServerCertificate=False;ApplicationIntent=ReadWrite;MultiSubnetFailover=False",
+        "MvcMovieIdentityContextConnection": "Data Source=192.168.50.11;Initial Catalog=TestIdentityDB;User ID=test_db_user;Password=!test_password001;Connect Timeout=30;Encrypt=False;TrustServerCertificate=False;ApplicationIntent=ReadWrite;MultiSubnetFailover=False"
    }
}

DBのマイグレーション

使うDBが変わったので、再度マイグレーション

PM> Update-Database -Context MvcMovieContext
PM> Update-Database -Context MvcMovieIdentityContext

発行して動作確認

発行

発行

vagrant の立ち上げ

$ vagrant up

dockerの立ち上げ

bin\up.sh
#!/bin/bash

# このシェルスクリプトのディレクトリの絶対パスを取得。
bin_dir=$(cd $(dirname $0) && pwd)
parent_dir=$bin_dir/..
docker_dir=$parent_dir/docker

# up.sh docker-compose.camp.yml
composeFile=${1:-"docker-compose.yml"}

# docker-composeの起動
cd $docker_dir && docker-compose -f $composeFile up
$ vagrant ssh
[vagrant@localhost vagrant]$ cd /vagrant/
[vagrant@localhost vagrant]$ ./bin/up.sh

確認

ブラウザで、 http://192.168.50.11/ にアクセス。

認証を確認したら以下のエラーが。。

Error.
An error occurred while processing your request.
Request ID: 0HLMS1B3T1BRS:00000001

Development Mode
Swapping to Development environment will display more detailed information about the error that occurred.

The Development environment shouldn't be enabled for deployed applications. It can result in displaying sensitive information from exceptions to end users. For local debugging, enable the Development environment by setting the ASPNETCORE_ENVIRONMENT environment variable to Development and restarting the app.
エラーの解決

Add-Migration CreateIdentitySchema -Context MvcMovieIdentityContextを抜かしていた。

PM> Add-Migration CreateIdentitySchema -Context MvcMovieIdentityContext
PM> Update-Database -Context MvcMovieIdentityContext

認証のマイグレーションの追加後ソース

ビルドしなおして、再度発行して、再度dockerを立ち上げ。
http://192.168.50.11/ で正常に動くことを確認。

参考

asp dotnet core

Visual Studio 2017の前提条件
推奨ラーニングパス
tutorial
dotnet core guide
dotnet core hello world
asp dotnet core mvc tutorial
sdk2.2
efcore tutorial
ASP.NET Core Identity をテンプレートからカスタマイズ
ASP.NET Identity メモ
ASP.NET Core アプリを Ubuntu サーバーで公開
.NET Coreアプリをさくっと作ってコンテナにする
ASP.NET Core MVCを新人に説明してみよう
CentOS 7.2上に.NET Core 1.1をインストールして、Visual StudioでビルドしたASP.NET Core Webアプリをデプロイしてサービス化する
ASP.NET Core 2.0 Authentication
Test
ASP.NET Core MVC アプリのテスト
xUnit 単体テスト 入門 in .NET Core : Assert の基礎
コントローラー ロジックの単体テスト
ASP.NET Core MVC と xUnit.NET でユニットテストを行う Part.2
コードの単体テスト
【Linux CentOS 7】VS2017で作成したサンプル.NET Core 2.0 MVC WEBアプリをLinuxでそのまま実行させる【.NET Core 2.0】
インフラストラクチャ
moq で 非同期メソッドの返り値を設定する
テスト エクスプローラーでテストを実行する
ASP.NET MVC の Repository パターン再考 .. DbContextのMockの作り方例
ASP.NET Core Web API – Repository Pattern
コードカバレッジの導入
dotnet core 2.2 docker image
dotnet command
複数のdcoker-compose で同じコンテナ内
ASP .NET Core で作った Web API を Linux サーバーでホストしてみる
portなど
ASP.NET CoreのWebApplicationを外部公開
Asp.net Identity
Asp.net Authorize
ログについて
Nlog ... Nlogのインストール後にVisual Studio の再起動をしないとNugetパッケージが認識されなかった。
log4net vs nlog

asp.net core 事例でおもしろそうなものメモ

ASP.NET CoreとVue.jsとHTTP Streamingでオンラインゲーム作ってみたおはなし

その他

画面操作などをGIFで録画できる「ScreenToGif」を利用して直感的に伝える

3
5
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
5