單元測試 ASP.NET Web API 2 時模擬 Entity Framework
本指引和應用程式旨在示範如何為使用 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 使用者入門。
教學課程中使用的軟體版本
- Visual Studio 2017
- Web API 2
本主題內容
本主題包含下列幾節:
如果您已完成使用 ASP.NET Web API 2 進行單元測試中的步驟,您可以跳至新增控制器一節。
必要條件
Visual Studio 2017 Community、Professional 或 Enterprise 版。
下載程式碼
下載已完成的專案。 可下載的專案包含本主題的單元測試程式碼,以及單元測試 ASP.NET Web API 2 主題。
使用單元測試專案建立應用程式
您可以在建立應用程式時建立單元測試專案,或將單元測試專案新增至現有的應用程式。 本教學課程旨在示範如何在建立應用程式時建立單元測試專案。
建立一個名為 StoreApp 的新 ASP.NET Web 應用程式。
在 [新增 ASP.NET 專案] 視窗中,選取 [空白] 範本,並新增 Web API 的資料夾和核心參考。 選取 [新增單元測試] 選項。 單元測試專案會自動命名為 StoreApp.Tests。 您可保留此名稱。
建立應用程式之後,您會看到它包含兩個專案 - StoreApp 和 StoreApp.Tests。
建立模型類別
在您的 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 控制器 (包含動作)。
設定下列值:
- 控制器名稱:ProductController
- 模型類別:產品
- 資料內容類別:[選取填入以下值的 [新增資料內容] 按鈕]
按一下 [新增] 以使用自動產生的程式碼建立控制器。 程式碼包含建立、擷取、更新和刪除 [產品] 類別執行個體的方法。 下列程式碼顯示用於新增 [產品] 的方法。 請注意,此方法會傳回 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 類別的執行個體。 您將使用稱為相依性插入的模式來修改您的應用程式,並移除該硬式編碼的相依性。 藉由中斷此相依性,您可以在測試時傳入模擬物件。
以滑鼠右鍵按一下 [模型] 資料夾,然後新增名為 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 專案,才能將套件新增至該專案。
從線上套件,尋找並安裝 EntityFramework 套件 (6.0 版或更新版本)。 如果它似乎已安裝 EntityFramework 套件,您可能已選取 StoreApp 專案,而不是 StoreApp.Tests 專案。
尋找並安裝 Microsoft ASP.NET Web API 2 核心套件。
關閉 Manage NuGet Packages 視窗。
建立測試內容
將名為 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 屬性標示的方法都會經過測試。 從 [測試] 功能表項執行測試。
開啟 [測試總管] 視窗,並注意測試結果。