通过


在单元测试 ASP.NET Web API 2 时模拟实体框架

作者: 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 社区版、专业版或企业版

下载代码

下载 已完成的项目。 可下载的项目包括本主题的单元测试代码和 单元测试 ASP.NET Web API 2 主题。

使用单元测试项目创建应用程序

可以在创建应用程序时创建单元测试项目,也可以向现有应用程序添加单元测试项目。 本教程介绍如何在创建应用程序时创建单元测试项目。

创建名为 StoreApp 的新 ASP.NET Web 应用程序。

在“新建 ASP.NET 项目”窗口中,选择 “空 ”模板,并为 Web API 添加文件夹和核心引用。 选择 “添加单元测试 ”选项。 单元测试项目自动命名为 StoreApp.Tests。 可以保留此名称。

创建单元测试项目

创建应用程序后,会看到它包含两个项目 - StoreApp 和 StoreApp.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; }
    }
}

生成解决方案。

添加控制器

右键单击“控制器”文件夹,然后选择“ 添加 ”和“ 新建基架项”。 使用 Entity Framework 选择包含操作的 Web API 2 控制器。

添加新控制器

设置以下值:

  • 控制器名称: ProductController
  • 模型类: 产品
  • 数据上下文类:[选择填充下面所示值的 “新建数据上下文 ”按钮]

指定控制器

单击“ 添加” 以使用自动生成的代码创建控制器。 该代码包括用于创建、检索、更新和删除 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 项目。

添加实体框架

查找并安装 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 属性标记的所有方法。 从 “测试 ”菜单项运行测试。

运行测试

打开 “测试资源管理器” 窗口,并注意测试结果。

测试结果