概要
単体テストのサンプルと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
リポジトリ作成
単体テストのチュートリアルでは、リポジトリパターンを使っているので、スキャフォールディングで作られたソースを修正する。
インターフェースはCore/Interfacesに、リポジトリはInfrastructureに作成する。
リポジトリパターン追加時ソース
インターフェース作成
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);
}
}
using MvcMovie.Core.Models;
namespace MvcMovie.Core.Interfaces
{
public interface IMovieRepository : IRepository<Movie, int> { }
}
リポジトリ作成
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);
}
}
}
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);
}
}
}
コントローラ修正
+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) の設定をする。
+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で作成する。
テストプロジェクトの追加
以下を参考に、テストプロジェクトを追加する
- ASP.NET Core のコントローラーのロジックをテストする
[ファイル] > [新規作成] > [Project]
-
xUnitテストプロジェクトを選択
テストの追加
- 以下のようなソースを作成する
- この時点だと依存関係が解決されていないため、依存関係を解決してやる
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]
Moqの参照の追加
リポジトリとコントローラを切り離すためのモックを作るライブラリを追加する。
プロジェクトを右クリックし、[追加]>[Nugetパッケージの管理]。
Moqで検索。インストール。
Microsoft.AspNetCore.Mvc.Abstractionsの参照の追加
プロジェクトを右クリックし、[追加]>[Nugetパッケージの管理]。
Microsoft.AspNetCore.Mvc.Abstractionsで検索。インストール。
Microsoft.AspNetCore.Mvc.ViewFeaturesの参照の追加
プロジェクトを右クリックし、[追加]>[Nugetパッケージの管理]。
Microsoft.AspNetCore.Mvc.Abstractionsで検索。インストール。
テストの実行
環境の準備については以下。
sql serverの準備
認証
認証のスキャフォールディング
- ログイン・ログアウト・登録の機能を追加。
- 同じ名前のContextは付けられないので別の名前のcontextを付ける。今回はMvcMovieIdentityContext
承認の有効化
--- 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つ追加する。
- 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 オブジェクトエクスプローラー]をクリック
SQL Serverを右クリックして、[SQL Serverの追加] を選択
使用するDBの確認
- TestDB, TestIdentityDBがあることを確認する。
- データベースをクリックし、表示されるプロパティから接続文字列を確認
接続の更新
SqlServerを使用に切り替えたソース ... ※ 認証のマイグレーションをこの時点では忘れている
{
"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/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でオンラインゲーム作ってみたおはなし