在單元測試 ASP.NET Web API 2 時模擬 Entity Framework

作者:Tom FitzMacken

下載已完成的專案

本指引和應用程式示範如何為使用 Entity Framework 的 Web API 2 應用程式建立單元測試。 它示範如何修改 Scaffold 控制器,以啟用傳遞用於測試的內容物件,以及如何建立使用 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。 您可以保留此名稱。

建立單元測試專案

建立應用程式之後,您會看到它包含兩個專案 : StoreAppStoreApp.Tests

建立模型類別

在您的 StoreApp 專案中,將類別檔案新增至名為Product.csModels資料夾。 以下列程式碼取代檔案的內容。

using System;

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

建置方案。

新增控制器

以滑鼠右鍵按一下 Controllers 資料夾,然後選取 [ 新增 ] 和 [ 新增 Scaffolded 專案]。 使用 Entity Framework 選取具有動作的 Web API 2 控制器。

新增控制器

設定下列值:

  • 控制器名稱: ProductController
  • 模型類別: Product
  • 資料內容類別別:[選取填入下列值的新 資料內容 按鈕]

指定控制器

按一下 [新增 ] 以使用自動產生的程式碼建立控制器。 此程式碼包含建立、擷取、更新和刪除 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 的其中一項新功能,可簡化單元測試開發。

在下一節中,您將自訂產生的程式碼,以協助將測試物件傳遞至控制器。

新增相依性插入

目前,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 中再進行一項變更。 在 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 核心套件包含在測試專案中。

以滑鼠右鍵按一下 StoreApp.Tests 專案,然後選取 [ 管理 NuGet 套件]。 您必須選取 StoreApp.Tests 專案,才能將套件新增至該專案。

管理套件

從 Online 套件中,尋找並安裝 EntityFramework 套件 (6.0 版或更新版本) 。 如果它似乎已安裝 EntityFramework 套件,您可能已選取 StoreApp 專案,而不是 StoreApp.Tests 專案。

新增 Entity Framework

尋找並安裝 Microsoft ASP.NET Web API 2 核心套件。

安裝 Web API 核心套件

關閉 [管理 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 屬性標記的方法都會經過測試。 從 [ 測試] 功能表項目,執行測試。

執行測試

開啟 [ 測試總管] 視窗,並注意測試結果。

測試結果