ASP.NET Web API 2 の単体テスト時に Entity Framework のモックを作成する

Tom FitzMacken

完成したプロジェクトをダウンロードする

このガイダンスとアプリケーションでは、Entity Framework を使用する Web API 2 アプリケーションの単体テストを作成する方法を示します。 スキャフォールディングされたコントローラーを変更して、テスト用のコンテキスト オブジェクトを渡せるようにする方法と、Entity Framework で動作するテスト オブジェクトを作成する方法を示します。

ASP.NET Web API を使用した単体テストの概要については、「ASP.NET Web API 2 を使用した単体テスト」を参照してください。

このチュートリアルでは、ASP.NET Web API の基本的な概念を理解していることを前提としています。 入門チュートリアルについては、「ASP.NET Web API 2 の概要」を参照してください。

チュートリアルで使用するソフトウェアのバージョン

このトピックの内容

このトピックは、次のセクションで構成されています。

ASP.NET Web API 2 を使用した単体テストの手順を既に完了している場合は、「コントローラーの追加」セクションをスキップできます。

前提条件

Visual Studio 2017 (Community、Professional、または Enterprise Edition)

コードをダウンロードする

完成したプロジェクトをダウンロードします。 ダウンロード可能なプロジェクトには、このトピックの単体テスト コードと、単体テスト ASP.NET Web API 2 トピックが含まれています。

単体テスト プロジェクトを使用してアプリケーションを作成する

アプリケーションの作成時に単体テスト プロジェクトを作成するか、既存のアプリケーションに単体テスト プロジェクトを追加することができます。 このチュートリアルでは、アプリケーションの作成時に単体テスト プロジェクトを作成する方法を示します。

StoreApp という名前の新しい ASP.NET Web アプリケーションを作成します。

[新しい ASP.NET プロジェクト] ウィンドウで、空のテンプレートを選択し、Web API のフォルダーとコア参照を追加します。 [単体テストの追加] オプションを選択します。 単体テスト プロジェクトには、StoreApp.Tests という名前が自動的に付けられます。 この名前は保持できます。

create unit test project

アプリケーションを作成すると、StoreAppStoreApp.Tests の 2 つのプロジェクトが含まれていることがわかります。

モデル クラスを作成する

StoreApp プロジェクトで、Product.cs という名前の Models フォルダーにクラス ファイルを追加します。 このファイルの内容を次のコードに置き換えます。

using System;

namespace StoreApp.Models
{
    public class Product
    {
        public int Id { get; set; }
        public string Name { get; set; }
        public decimal Price { get; set; }
    }
}

ソリューションをビルドします。

コントローラーの追加

Controllers フォルダーを右クリックし、[追加][新しいスキャフォールディングされた項目] の順に選択します。 Entity Framework を使用したアクションがある Web API 2 コントローラーを選択します。

add new controller

次の値を設定します。

  • Controller 名: ProductController
  • Model クラス: Product
  • データ コンテキスト クラス: [次に示す値を入力する [新しいデータ コンテキスト] ボタンを選択します]

specify controller

[追加] をクリックして、自動的に生成されたコードを含むコントローラーを作成します。 このコードには、Product クラスのインスタンスを作成、取得、更新、削除するためのメソッドが含まれています。 次のコードは、Product を追加する方法を示しています。 メソッドが IHttpActionResult のインスタンスを返していることに注意してください。

// POST api/Product
[ResponseType(typeof(Product))]
public IHttpActionResult PostProduct(Product product)
{
    if (!ModelState.IsValid)
    {
        return BadRequest(ModelState);
    }

    db.Products.Add(product);
    db.SaveChanges();

    return CreatedAtRoute("DefaultApi", new { id = product.Id }, product);
}

IHttpActionResult は Web API 2 の新機能の 1 つであり、単体テストの開発を簡素化します。

次のセクションでは、生成されたコードをカスタマイズして、テスト オブジェクトをコントローラーに渡しやすくします。

依存関係の挿入を追加する

現在、ProductController クラスは、StoreAppContext クラスのインスタンスを使用するようにハードコーディングされています。 依存関係の挿入と呼ばれるパターンを使用して、アプリケーションを変更し、ハードコーディングされた依存関係を削除します。 この依存関係を解除することで、テスト時にモック オブジェクトを渡すことができます。

Models フォルダーを右クリックし、IStoreAppContext という名前新しいインターフェイスを追加します。

このコードを次のコードに置き換えます。

using System;
using System.Data.Entity;

namespace StoreApp.Models
{
    public interface IStoreAppContext : IDisposable
    {
        DbSet<Product> Products { get; }
        int SaveChanges();
        void MarkAsModified(Product item);    
    }
}

StoreAppContext.cs ファイルを開き、次の強調表示された変更を行います。 注意すべき重要な変更点は次のとおりです。

  • StoreAppContext クラスが IStoreAppContext インターフェイスを実装する
  • MarkAsModified メソッドが実装されている
using System;
using System.Data.Entity;

namespace StoreApp.Models
{
    public class StoreAppContext : DbContext, IStoreAppContext
    {
        public StoreAppContext() : base("name=StoreAppContext")
        {
        }

        public DbSet<Product> Products { get; set; }
    
        public void MarkAsModified(Product item)
        {
            Entry(item).State = EntityState.Modified;
        }
    }
}

ProductController.cs ファイルを開きます。 強調表示されているコードと一致するように既存のコードを変更します。 これらの変更により、StoreAppContext への依存関係が解消され、他のクラスがコンテキスト クラスの別のオブジェクトを渡すことができます。 この変更により、単体テスト中にテスト コンテキストで渡すことができます。

public class ProductController : ApiController
{
    // modify the type of the db field
    private IStoreAppContext db = new StoreAppContext();

    // add these constructors
    public ProductController() { }

    public ProductController(IStoreAppContext context)
    {
        db = context;
    }
    // rest of class not shown
}

ProductController でもう 1 つの変更を行う必要があります。 PutProduct メソッドで、エンティティの状態を変更済みに設定する行を MarkAsModified メソッドの呼び出しに置き換えます。

// PUT api/Product/5
public IHttpActionResult PutProduct(int id, Product product)
{
    if (!ModelState.IsValid)
    {
        return BadRequest(ModelState);
    }

    if (id != product.Id)
    {
        return BadRequest();
    }

    //db.Entry(product).State = EntityState.Modified;
    db.MarkAsModified(product);
    
    // rest of method not shown
}

ソリューションをビルドします。

これで、テスト プロジェクトを設定する準備ができました。

テスト プロジェクトに NuGet パッケージをインストールする

空のテンプレートを使用してアプリケーションを作成する場合、単体テスト プロジェクト (StoreApp.Tests) にはインストールされている NuGet パッケージは含まれません。 Web API テンプレートなどの他のテンプレートには、単体テスト プロジェクトにいくつかの NuGet パッケージが含まれています。 このチュートリアルでは、Entity Framework パッケージと Microsoft ASP.NET Web API 2 Core パッケージをテスト プロジェクトに含める必要があります。

StoreApp.Tests プロジェクトを右クリックし、[NuGet パッケージの管理] を選択します。 パッケージをそのプロジェクトに追加するには、StoreApp.Tests プロジェクトを選択する必要があります。

manage packages

オンライン パッケージから EntityFramework パッケージ (バージョン 6.0 以降) を見つけてインストールします。 EntityFramework パッケージが既にインストールされているように見える場合は、StoreApp.Tests プロジェクトではなく StoreApp プロジェクトを選択している可能性があります。

add Entity Framework

Microsoft ASP.NET Web API 2 Core パッケージを見つけてインストールします。

install web api core package

[NuGet パッケージの管理] ウィンドウを閉じます。

テスト コンテキストを作成する

TestDbSet という名前のクラスをテスト プロジェクトに追加します。 このクラスは、テスト データ セットの基底クラスとして機能します。 このコードを次のコードに置き換えます。

using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Data.Entity;
using System.Linq;

namespace StoreApp.Tests
{
    public class TestDbSet<T> : DbSet<T>, IQueryable, IEnumerable<T>
        where T : class
    {
        ObservableCollection<T> _data;
        IQueryable _query;

        public TestDbSet()
        {
            _data = new ObservableCollection<T>();
            _query = _data.AsQueryable();
        }

        public override T Add(T item)
        {
            _data.Add(item);
            return item;
        }

        public override T Remove(T item)
        {
            _data.Remove(item);
            return item;
        }

        public override T Attach(T item)
        {
            _data.Add(item);
            return item;
        }

        public override T Create()
        {
            return Activator.CreateInstance<T>();
        }

        public override TDerivedEntity Create<TDerivedEntity>()
        {
            return Activator.CreateInstance<TDerivedEntity>();
        }

        public override ObservableCollection<T> Local
        {
            get { return new ObservableCollection<T>(_data); }
        }

        Type IQueryable.ElementType
        {
            get { return _query.ElementType; }
        }

        System.Linq.Expressions.Expression IQueryable.Expression
        {
            get { return _query.Expression; }
        }

        IQueryProvider IQueryable.Provider
        {
            get { return _query.Provider; }
        }

        System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
        {
            return _data.GetEnumerator();
        }

        IEnumerator<T> IEnumerable<T>.GetEnumerator()
        {
            return _data.GetEnumerator();
        }
    }
}

TestProductDbSet という名前のクラスを、次のコードを含むテスト プロジェクトに追加します。

using System;
using System.Linq;
using StoreApp.Models;

namespace StoreApp.Tests
{
    class TestProductDbSet : TestDbSet<Product>
    {
        public override Product Find(params object[] keyValues)
        {
            return this.SingleOrDefault(product => product.Id == (int)keyValues.Single());
        }
    }
}

TestStoreAppContext という名前のクラスを追加し、既存のコードを次のコードに置き換えます。

using System;
using System.Data.Entity;
using StoreApp.Models;

namespace StoreApp.Tests
{
    public class TestStoreAppContext : IStoreAppContext 
    {
        public TestStoreAppContext()
        {
            this.Products = new TestProductDbSet();
        }

        public DbSet<Product> Products { get; set; }

        public int SaveChanges()
        {
            return 0;
        }

        public void MarkAsModified(Product item) { }
        public void Dispose() { }
    }
}

テストの作成

既定では、テスト プロジェクトには UnitTest1.cs という名前の空のテスト ファイルが含まれています。 このファイルには、テスト メソッドの作成に使用する属性が表示されます。 このチュートリアルでは、新しいテスト クラスを追加するため、このファイルを削除できます。

TestProductController という名前のクラスをテスト プロジェクトに追加します。 このコードを次のコードに置き換えます。

using System;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using System.Web.Http.Results;
using System.Net;
using StoreApp.Models;
using StoreApp.Controllers;

namespace StoreApp.Tests
{
    [TestClass]
    public class TestProductController
    {
        [TestMethod]
        public void PostProduct_ShouldReturnSameProduct()
        {
            var controller = new ProductController(new TestStoreAppContext());

            var item = GetDemoProduct();

            var result =
                controller.PostProduct(item) as CreatedAtRouteNegotiatedContentResult<Product>;

            Assert.IsNotNull(result);
            Assert.AreEqual(result.RouteName, "DefaultApi");
            Assert.AreEqual(result.RouteValues["id"], result.Content.Id);
            Assert.AreEqual(result.Content.Name, item.Name);
        }

        [TestMethod]
        public void PutProduct_ShouldReturnStatusCode()
        {
            var controller = new ProductController(new TestStoreAppContext());

            var item = GetDemoProduct();

            var result = controller.PutProduct(item.Id, item) as StatusCodeResult;
            Assert.IsNotNull(result);
            Assert.IsInstanceOfType(result, typeof(StatusCodeResult));
            Assert.AreEqual(HttpStatusCode.NoContent, result.StatusCode);
        }

        [TestMethod]
        public void PutProduct_ShouldFail_WhenDifferentID()
        {
            var controller = new ProductController(new TestStoreAppContext());

            var badresult = controller.PutProduct(999, GetDemoProduct());
            Assert.IsInstanceOfType(badresult, typeof(BadRequestResult));
        }

        [TestMethod]
        public void GetProduct_ShouldReturnProductWithSameID()
        {
            var context = new TestStoreAppContext();
            context.Products.Add(GetDemoProduct());

            var controller = new ProductController(context);
            var result = controller.GetProduct(3) as OkNegotiatedContentResult<Product>;

            Assert.IsNotNull(result);
            Assert.AreEqual(3, result.Content.Id);
        }

        [TestMethod]
        public void GetProducts_ShouldReturnAllProducts()
        {
            var context = new TestStoreAppContext();
            context.Products.Add(new Product { Id = 1, Name = "Demo1", Price = 20 });
            context.Products.Add(new Product { Id = 2, Name = "Demo2", Price = 30 });
            context.Products.Add(new Product { Id = 3, Name = "Demo3", Price = 40 });

            var controller = new ProductController(context);
            var result = controller.GetProducts() as TestProductDbSet;

            Assert.IsNotNull(result);
            Assert.AreEqual(3, result.Local.Count);
        }

        [TestMethod]
        public void DeleteProduct_ShouldReturnOK()
        {
            var context = new TestStoreAppContext();
            var item = GetDemoProduct();
            context.Products.Add(item);

            var controller = new ProductController(context);
            var result = controller.DeleteProduct(3) as OkNegotiatedContentResult<Product>;

            Assert.IsNotNull(result);
            Assert.AreEqual(item.Id, result.Content.Id);
        }

        Product GetDemoProduct()
        {
            return new Product() { Id = 3, Name = "Demo name", Price = 5 };
        }
    }
}

テストの実行

これで、テストを実行する準備が整いました。 TestMethod 属性でマークされているすべてのメソッドがテストされます。 [テスト] メニュー項目から、テストを実行します。

run tests

[テスト エクスプローラー] ウィンドウを開き、テストの結果を確認します。

test results