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

作者 Tom FitzMacken

下载已完成项目

本指南和应用程序演示如何为使用实体框架的 Web API 2 应用程序创建单元测试。 它演示如何修改基架控制器以允许传递上下文对象进行测试,以及如何创建使用实体框架的测试对象。

有关使用 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。 可以保留此名称。

创建单元测试项目

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

生成解决方案。

添加控制器

右键单击“控制器”文件夹,然后选择 “添加 ”和“ 新建基架项”。 使用实体框架选择包含操作的 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 项目才能将包添加到该项目。

管理包

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

运行测试

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

测试结果