はじめに
アプリケーションを開発する際に、テストは必須なのですが、データベースのMoqを作るのも面倒だったり、設定ファイルやコードの一部をテストのたびに変更するのが面倒でした。
本番環境でもテストでもコードを変更せずに使えるようにしたいなと思い、試行錯誤・・・ようやく一つの形ができたので、記事として紹介させていただこうと思いました。
DbContextをDIを利用して...とやってみたものの、PrismのUnityコンテナーを使用したところ、うまく動作しないことがあり、ASP.NETとかならServiceCollectionを使用してと思うのですが、デスクトップアプリでしかもPrismだと使えなかったりといろいろと課題がありました。
とりあえず、MoqではなくSqliteInMemoryを使用することにし、MictrosoftのSQLite を使用した EF Core アプリケーションのテストを参考にしました。
ところがテスト一つだけなら動きますが、複数のテストがあると、うまくいきませんでした。そこで調べ続けているうちにデータ ポイント - EF Core と InMemory プロバイダーを使ったテスト作成のヒントという記事を見つけました。
これがとても参考になり、DbContextOptionsを利用したテスト環境を構築することができました。
環境準備
EF Coreを使用したデータベース利用環境です。初めて作成する人向けの部分ですので、不要の方は読み飛ばして、次のDBDbContextOptionsFactory作成からお読みください。
Visual Studioで新規プロジェクト作成
ここでは、Prism環境を前提にしたサンプルですので、シンプルなPrism Blank App(.NET Core)で作成します。
ちなみに今回はテスト環境のため、ViewやViewModelは触りません。
使用するNuGetパッケージ
EF Core関係のNuGetパッケージ
・Microsoft.EntityFrameworkCore
・Microsoft.EntityFrameworkCore.Design
・Microsoft.EntityFrameworkCore.SqlServer
・Microsoft.EntityFrameworkCore.Sqlite
・Microsoft.EntityFrameworkCore.Tools
SampleModelとSampleDbContextを作成
クラス作成
Infrastructuresフォルダをプロジェクト下に作成して、その中にSampleModelクラスとSampleDbContextクラスを作成します(フォルダ名やクラス名はサンプル用のためで適当です)。
ここまででソリューション構成はこんな感じになっていると思います
SampleModelとSampleDbContextのコーディング
SampleModel
データベースとして使用するサンプルモデルを作成します
public class SampleModel
{
public int SampleModelId { get; set; }
public string Message { get; set; }
}
SampleDbContext
EF Coreを使用したDbContextのコードを作成します
public class SampleDbContext : DbContext
{
public SampleDbContext(DbContextOptions<SampleDbContext> options) : base(options)
{
}
public DbSet<SampleModel> SampleModels { get; set; }
}
テータベース設定用のクラス
DbSettingクラス
仰々しく書いていますが、使用するデータベースは何か設定するただのStaticクラスです。
接続文字列をここに書けるようにしていますが、設定ファイルから読み込んだり、JSONを使用したりといろいろありますので、別にこういうクラスではなくても、そのあたりは読者の方々の環境によって変更してもらえればと思います。
/// <summary>
/// 接続するデータベース種別
/// </summary>
public static class DbSetting
{
public static string DataBaseName { get; set; } = "SQLServer";
public static string SQLServerConnectionString { get; set; }
public static string SqliteInMemoryConnectionString { get; set; }
}
ここまでで、基本的な環境構築は終わったので、次から、今回の肝となるDbContextOptionsFactoryの作成をします
DbContextOptionsFactory作成
インターフェース作成
PrismのDIコンテナーを使用して、オブジェクト注入できるようにインターフェースを作成します
Infrastructuresフォルダ内にIDbContextOptionsFactoryクラスを作成して以下のコードを記述します
public interface IDbContextOptionsFactory
{
DbContextOptions<SampleDbContext> Options { get; }
}
インターフェース実装
Infrastructuresフォルダ内にDbContextOptionsFactoryクラスというインターフェース実装コーディングをします。
中身としては、要するにDbContextOptionを指定されたDBによって作成するファクトリークラスです
public class DbContextOptionsFactory : IDbContextOptionsFactory
{
private DbConnection _dbConnection;
public DbContextOptions<SampleDbContext> Options { get; private set; }
public DbContextOptionsFactory()
{
SetDbContextOptions();
}
private void SetDbContextOptions()
{
switch (DbSetting.DataBaseName)
{
// 本番環境
case "SQLServer":
{
var option = new DbContextOptionsBuilder<SampleDbContext>();
Options = option.UseSqlServer(DbSetting.SQLServerConnectionString).Options;
break;
}
// テスト環境
case "SqliteInMemory":
{
if (_dbConnection == null)
{
_dbConnection = new SqliteConnection(DbSetting.SqliteInMemoryConnectionString);
_dbConnection.Open();
var option = new DbContextOptionsBuilder<SampleDbContext>();
// InMemoryではトランザクション(スコープ)を使うとエラーになってしまうので回避コード
option.ConfigureWarnings(x => x.Ignore(Microsoft.EntityFrameworkCore.Diagnostics.RelationalEventId.AmbientTransactionWarning));
Options = option.UseSqlite(_dbConnection).Options;
using (var context = new SampleDbContext(Options))
{
context.Database.EnsureDeleted();
context.Database.EnsureCreated();
}
}
break;
}
}
}
}
SqliteInMemoryでは、DbConnectionがnullの場合のみDBを作成して、使いまわしができるようにしています。
あと、option.ConfigureWarnings(x => x.Ignore(Microsoft.EntityFrameworkCore.Diagnostics.RelationalEventId.AmbientTransactionWarning));
というコードは、トランザクションを使用していると、AmbientTransactionWarningが発生して、テストが失敗してしまうので、それを回避するためのものです。
DIコンテナーへの登録
Prismを使用する場合、コンテナーに登録することでDIを行うことができるようになります。
App.xamlを開いて、その中のRegisterTypesで以下のようにコーディングします
protected override void RegisterTypes(IContainerRegistry containerRegistry)
{
containerRegistry.Register<IDbContextOptionsFactory, DbContextOptionsFactory>();
}
以上で環境構築は完了です。
書いてしまうと、シンプルなんですが、ここまで来るのに、いろいろと試行錯誤を重ねました。
最後にテストを実際に走らせてみようと思います。
データアクセスクラス作成
Repositoryとなるものですが、今回はサンプルなので単純なDbAccessクラスを作成して、Add, Delete, Updateのコードを記述します。
ポイントは、先述したIDbContextOptionsFactoryをDIで使用しているところです。
public class DbAccess
{
private readonly IDbContextOptionsFactory _dbContextOptionsFactory;
public DbAccess(IDbContextOptionsFactory dbContextOptionsFactory)
{
_dbContextOptionsFactory = dbContextOptionsFactory;
}
/// <summary>
/// 登録されているメッセージの数
/// </summary>
/// <returns></returns>
public int CountMessage()
{
using (SampleDbContext sampleDbContext = new SampleDbContext(_dbContextOptionsFactory.Options))
{
return sampleDbContext.SampleModels.Count();
}
}
/// <summary>
/// 新規メッセージを登録します
/// </summary>
/// <param name="message"></param>
public void AddMessage(string message)
{
using (SampleDbContext sampleDbContext = new SampleDbContext(_dbContextOptionsFactory.Options))
{
var model = sampleDbContext.SampleModels.Where(x => x.Message == message).FirstOrDefault();
if(model == null)
{
sampleDbContext.SampleModels.Add(new SampleModel()
{
Message = message
});
sampleDbContext.SaveChanges();
}
}
}
/// <summary>
/// メッセージの削除
/// </summary>
/// <param name="message"></param>
public void DeleteMessage(string message)
{
using (SampleDbContext sampleDbContext = new SampleDbContext(_dbContextOptionsFactory.Options))
{
var model = sampleDbContext.SampleModels.Where(x => x.Message == message).FirstOrDefault();
if (model != null)
{
sampleDbContext.SampleModels.Remove(model);
sampleDbContext.SaveChanges();
}
}
}
/// <summary>
/// メッセージに変更
/// </summary>
/// <param name="id"></param>
/// <param name="message"></param>
public void UpdateMessage(int id, string message)
{
using (SampleDbContext sampleDbContext = new SampleDbContext(_dbContextOptionsFactory.Options))
{
var model = sampleDbContext.SampleModels.Where(x => x.SampleModelId == id).FirstOrDefault();
if(model != null)
{
model.Message = message;
sampleDbContext.SaveChanges();
}
}
}
}
テストプロジェクト作成
新しくMSTestプロジェクトを作成します。
テストの一例です
namespace TestProject
{
[TestClass]
public class UnitTest1
{
DbContextOptionsFactory dbContextOptionsFactory;
int testId = 0;
/// <summary>
/// コンストラクタ
/// </summary>
public UnitTest1()
{
// SqliteInMemoryに設定
DbSetting.DataBaseName = "SqliteInMemory";
// DB名はテストごとに変更すると競合しなくてすみます
DbSetting.SqliteInMemoryConnectionString = $"DataSource=UnitTest1.db;mode=memory;";
// DIは使用せずに直接実装から呼び出しています
dbContextOptionsFactory = new DbContextOptionsFactory();
//初期データ作成
Seed();
}
/// <summary>
/// 初期データ作成
/// using(DbContext...が複数あっても大丈夫
/// </summary>
[TestMethod("初期データ作成")]
private void Seed()
{
using (SampleDbContext sampleDbContext = new SampleDbContext(dbContextOptionsFactory.Options))
{
sampleDbContext.SampleModels.Add(new SampleModel()
{
Message = "おはよう"
});
sampleDbContext.SampleModels.Add(new SampleModel()
{
Message = "こんにちは"
});
sampleDbContext.SampleModels.Add(new SampleModel()
{
Message = "さようなら"
});
sampleDbContext.SaveChanges();
}
using (SampleDbContext sampleDbContext = new SampleDbContext(dbContextOptionsFactory.Options))
{
sampleDbContext.SampleModels.Add(new SampleModel()
{
Message = "おやすみなさい"
});
sampleDbContext.SampleModels.Add(new SampleModel()
{
Message = "いただきます"
});
sampleDbContext.SaveChanges();
}
using(SampleDbContext sampleDbContext = new SampleDbContext(dbContextOptionsFactory.Options))
{
var count = sampleDbContext.SampleModels.Count();
// 最初に登録されてたメッセージは5種類
Assert.IsTrue(count == 5);
// 後で使用するため、"おはよう"の主キーを取得しておきます
testId = sampleDbContext.SampleModels.Where(x => x.Message == "おはよう").Select(x => x.SampleModelId).FirstOrDefault();
}
}
[TestMethod("DbAccessのテスト")]
public void DbAccessTest()
{
DbAccess dbAccess = new DbAccess(dbContextOptionsFactory);
var count = dbAccess.CountMessage();
// 最初に登録されているメッセージは5種類
Assert.IsTrue(count == 5);
// 追加
// 既存のメッセージがあるので数は変更ないはず
dbAccess.AddMessage("おはよう");
count = dbAccess.CountMessage();
Assert.IsTrue(count == 5);
// 新しいメッセージを追加
dbAccess.AddMessage("ごちそうさま");
count = dbAccess.CountMessage();
Assert.IsTrue(count == 6);
// 削除
// 存在しないメッセージは削除できず数の変更なしのはず
dbAccess.DeleteMessage("おはようございます");
count = dbAccess.CountMessage();
Assert.IsTrue(count == 6);
// 変更
// おはよう => おはようございます
dbAccess.UpdateMessage(testId, "おはようございます");
count = dbAccess.CountMessage();
Assert.IsTrue(count == 6);
// 削除
// こんどは"おはようございます"は存在するので削除できる
dbAccess.DeleteMessage("おはようございます");
count = dbAccess.CountMessage();
Assert.IsTrue(count == 5);
}
}
}
テストの際にポイントとなるのはDataSourceのデータベース名を共有したいテストごとに名前を変更するということです。
ここではテスト名.dbとしていますが、こうすることで、別のテストを作成した場合は、別のインメモリーデータベースを使用することができるようになります。
こうすることで、初期データやテストデータを様々に変更したり、共有したりしながらテストを行うことができるようになります。
さいごに
今回はSqliteInMemoryを使用したテスト環境構築の一例を紹介させていただきましたが、DbContextOptionsFactoryに本番環境としてSQL Serverを使用するコードを記載しています。このあたりをいろいろといじれば、EF Coreでサポートされている形式に合わせてDbContextOptionを生成することができるようになると思います。