ASP.NET Core でインメモリ DB を使ってサービスクラスをテストする

## はじめに

インメモリ DB はその名の通り、メモリ内のデータベースです。
実際に DB を用意する必要がないのでモックやテストちょっとした動作確認に向いています。
また外部 IO がないので非常の高速に動作します。
ただし揮発性メモリなのでアプリを終了するとデータはすべて消えます。

## インメモリ DB を使ったサンプルプロジェクトとテスト

最終的なソリューション構成は以下の通りです。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/195024/85333e83-5d71-9f16-ff70-a00b39e828fc.png)


### 環境構築

```sh
# .NET Core のバージョン
dotnet --version
2.1.400
# ワーキングフォルダ
mkdir InMemoryDbSample
cd ./InMemoryDbSample
# ソリューションの作成
dotnet new sln
# インメモリ DB のサンプルプロジェクトを作成
dotnet new web -o ./InMemoryDbSample
# インメモリ DB を試す XUnit テストプロジェクトを作成
dotnet new xunit -o ./InMemoryDbTest
# ソリューションにプロジェクトに追加
dotnet sln add ./InMemoryDbSample ./InMemoryDbTest
# テストのために InMemoryDbTest から InMemoryDbSample を参照
dotnet add ./InMemoryDbTest reference ./InMemoryDbSample
# テストプロジェクトでもインメモリ DB を使うため Nuget からパッケージを追加
dotnet add ./InMemoryDbTest package Microsoft.EntityFrameworkCore.InMemory
# 正常にビルドできるか確認
dotnet build
```

### サービスクラス

テスト対象です。
書籍管理アプリに初期追加、書籍検索を行うサービスクラスがあったとします。

```cs:
using InMemoryDb.Data;
using InMemoryDb.Data;
using InMemoryDbSample.Models;
using System.Collections.Generic;
using System.Linq;

namespace InMemoryDbSample.Services
{
    public class BooksService
    {
        // DI した DbContext を保持
        BooksDbContext Context { get; }

        // DbContext を DI（依存オブジェクトの注入）する
        public BooksService(BooksDbContext context) =>
            Context = context;

        // 書籍追加
        public void Add(Book book)
        {
            Context.Books.Add(book);
            Context.SaveChanges();
        }

        // 検索ワードがタイトルに含まれる書籍一覧を得る
        public IEnumerable<Book> GetBooks(string term) =>
            Context.Books
                .Where(book => book.Title.Contains(term))
                .OrderBy(book => book.ISBN)
                .ToArray();
    }
}
```

### エンティティ

書籍エンティティは以下の通りです。
`[Key]`アノテーションをつけると`ID`以外をキーにできます。

```cs:Models/Book.cs
using System.ComponentModel.DataAnnotations;

namespace InMemoryDbSample.Models
{
    public class Book
    {
        [Key] public string ISBN { get; set; }
        public string Title { get; set; }
    }
}
```

#### データベースコンテキスト

コンストラクタでの引数で`DbContextOptions<T>`を受け取ります。
使うときはインメモリ DB を使う設定を渡します。

```cs:Data/BooksDbContext.cs
using InMemoryDbSample.Models;
using Microsoft.EntityFrameworkCore;

namespace InMemoryDb.Data
{
    public class BooksDbContext : DbContext
    {
        public BooksDbContext(DbContextOptions<BooksDbContext> options)
            : base(options) { }

        public DbSet<Book> Books { get; set; }
    }
}
```

### インメモリ DB を使ってみる

テストプロジェクト`InMemoryDbTest`にインメモリ DB を使ったテストを書きます。
データベースコンテキストをインスタンス化する際に引数にインメモリ DB を使う設定を渡します。

```cs:InMemoryDbTest/BooksDbContextTest.cs
using InMemoryDb.Data;
using InMemoryDbSample.Models;
using InMemoryDbSample.Services;
using Microsoft.EntityFrameworkCore;
using System.Linq;
using Xunit;

namespace InMemoryDbTest
{
    public class BooksDbContextTest
    {
        [Fact]
        public void Add_writes_to_database()
        {
            // インメモリ DB を使うオプション
            var options = new DbContextOptionsBuilder<BooksDbContext>()
                .UseInMemoryDatabase(databaseName: "Add_writes_to_database")
                .Options;

            // 本を追加。※using ステートメントで一度 DB との接続を切る。
            using (var context = new BooksDbContext(options)) // インメモリ DB を使うオプション設定を渡す
            {
                // サービスクラスをインスタンス化しデータベースコンテキストを DI
                var service = new BooksService(context);
                service.AddBook(new Book
                {
                    ISBN = "978-4150117481",
                    Title = "月は無慈悲な夜の女王"
                });
            }

            // 再びインメモリ DB から値を取り出してテスト
            using (var context = new BooksDbContext(options))
            {
                
                Assert.Equal(1, context.Books.Count());
                var book = context.Books.Single(e => e.ISBN == "978-4150117481");
                Assert.Equal("月は無慈悲な夜の女王", book.Title);
            }
        }
    }
}
```

### テストの実行

上記のコードのユニットテストをしてインメモリ DB が使えているか確かめます。

```sh
dotnet test ./InMemoryDbTest
ビルドが開始されました。しばらくお待ちください...
ビルドが完了しました。

C:略/InMemoryDbSample/InMemoryDbTest/bin/Debug/netcoreapp2.1/InMemoryDbTest.dll(.NETCoreApp,Version=v2.1) のテスト実行
Microsoft (R) Test Execution Command Line Tool Version 15.8.0
Copyright (c) Microsoft Corporation.  All rights reserved.

テスト実行を開始しています。お待ちください...

テストの合計数: 1。成功: 1。失敗:0。スキップ: 0。
テストの実行に成功しました。
テスト実行時間: 1.9507 秒
```

![image.png](https://qiita-image-store.s3.amazonaws.com/0/195024/00badae1-e652-72e6-23a3-142b38ce9fc8.png)