65.9K
CodeProject 正在变化。 阅读更多。
Home

RESTful 日常 #7:使用 NUnit 和 Moq framework 在 WebAPI 中进行单元测试和集成测试(第一部分)

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.94/5 (38投票s)

2016年3月1日

CPOL

24分钟阅读

viewsIcon

101202

downloadIcon

4381

在本文中,我们将学习如何为核心业务逻辑编写单元测试,主要关注基本的CRUD操作。

目录

介绍

我们一直在学习WebAPI。我们几乎涵盖了使用ASP.NET WebAPI构建健壮的全栈REST服务所需的所有技术,从创建服务到使其成为安全且随时可用的企业级应用程序样板。在本文中,我们将学习如何专注于测试驱动开发,并为我们的服务端点和业务逻辑编写单元测试。我将使用NUnitMoq框架来为业务逻辑层和控制器方法编写测试用例。我将减少理论讲解,更多地关注如何使用这些框架编写单元测试的实际实现。我将文章分为两部分。第一部分侧重于测试业务逻辑和在我们代码库中创建为BusinessServices的类库。第二部分将侧重于测试Web API。分层的原因很简单;这篇文章的范围非常大,可能会变成一篇非常长的文章,难以一次性阅读完。

路线图

以下是我为逐步学习WebAPI设置的路线图

我特意使用了Visual Studio 2010和.NET Framework 4.0,因为有些实现在.NET Framework 4.0中很难找到,但我会通过展示如何做到这一点来使其变得容易。

单元测试

"单元测试允许你快速对代码进行大量修改。你知道它现在可以工作,因为你已经运行了测试,当你需要进行修改时,你需要让测试再次工作。这节省了数小时的时间。" 我从Stack Overflow上的一个帖子中看到了这句话,我完全同意这个说法。

良好的单元测试有助于开发人员理解其代码以及(最重要的是)业务逻辑。单元测试有助于理解业务逻辑的所有方面,从所需的输入和输出到代码可能失败的条件。代码具有良好编写的单元测试,只要单元测试覆盖了所有需要执行的测试用例,其失败的可能性就较小。

NUnit

有各种可用于单元测试的框架。NUnit是我偏爱的一个。NUnit与.NET配合良好,并提供了轻松编写单元测试的灵活性。它具有有意义且自解释的属性和类名,有助于开发人员轻松编写测试。NUnit提供了一个易于使用的交互式GUI,您可以在其中运行测试并获取详细信息。它以美观的方式显示通过或失败的测试数量,并且在任何测试失败时还会提供堆栈跟踪,从而使您可以在GUI本身执行第一级调试。我建议在您的机器上下载并安装NUnit以运行测试。我们将在编写完所有测试后使用NUnit GUI。我通常使用ReSharper集成在我的Visual Studio中提供的NUnit内置GUI。但是,我建议您使用NUnit GUI来运行测试,因为ReSharper是一个付费库,可能只有少数开发人员集成它。由于我们使用的是Visual Studio 2010,我们需要使用NUnit的旧版本,即2.6.4。您可以按照此URL下载并运行.msi文件并在您的机器上安装。

安装完成后,您将在计算机的已安装项目中看到NUnit,如下图所示

Moq 框架

Moq是一个简单直接的库,用于在C#中模拟对象。我们可以借助mock库模拟数据、仓库、类和实例。因此,在编写单元测试时,我们不会在实际的类实例上执行它们,而是通过创建类对象的代理来进行内存中的单元测试。与NUnit一样,Moq库的类也易于使用和理解。它几乎所有的方法、类和接口名称都是自解释的。

以下是摘自维基百科关于为什么要使用模拟对象的原因列表:

  • 对象提供非确定性结果(例如,当前时间或当前温度);
  • 具有不易创建或重现的状态(例如,网络错误);
  • 速度慢(例如,一个完整的数据库,在测试前必须初始化);
  • 尚不存在或行为可能改变;
  • 必须包含仅用于测试目的的信息和方法(而不是用于其实际任务)。

因此,我们编写的任何测试,实际上都是在测试数据和代理对象上执行的,即,而不是真实类的实例。我们将使用Moq来模拟数据和仓储,以便在执行单元测试时不会一遍又一遍地访问数据库。您可以在本文中阅读更多关于Moq的内容。

设置解决方案

我将使用这篇文章来解释如何为业务逻辑(即,涵盖我们的业务逻辑层和WebAPI控制器)编写单元测试。单元测试的范围不应仅限于业务逻辑或端点,而应扩展到所有公开的逻辑,如过滤器和处理程序。编写良好的单元测试应覆盖几乎所有代码。可以通过一些在线工具跟踪代码覆盖率。我们不会测试过滤器和通用类,但会专注于控制器和业务逻辑层,并了解如何进行单元测试。我将使用我们在系列第6天之前使用的相同源代码,并继续使用我们从系列最后一篇文章中得到的最新代码库。代码库可与此帖子一起下载。当您从我的上一篇文章中获取代码库并在Visual Studio中打开它时,您将看到的项目结构如下图所示

IUnitOfWork是我添加的新接口,用于促进接口驱动开发。它有助于模拟对象并改进结构和可读性。只需打开Visual Studio,并在DataModel项目下的UnitOfWork文件夹中添加一个名为IUnitOfWork的新接口,并定义UnitOfWork类中使用的属性,如下所示

现在,转到UnitOfWork类,并让该类继承此接口,因此UnitOfWork类变为如下所示

#region Using Namespaces...

using System;
using System.Collections.Generic;
using System.Data.Entity;
using System.Diagnostics;
using System.Data.Entity.Validation;
using DataModel.GenericRepository;

#endregion

namespace DataModel.UnitOfWork
{
    /// <summary>
    /// Unit of Work class responsible for DB transactions
    /// </summary>
    public class UnitOfWork : IDisposable, IUnitOfWork
    {
        #region Private member variables...

        private readonly WebApiDbEntities _context = null;
        private GenericRepository<User> _userRepository;
        private GenericRepository<Product> _productRepository;
        private GenericRepository<Token> _tokenRepository;
        #endregion

        public UnitOfWork()
        {
            _context = new WebApiDbEntities();
        }

        #region Public Repository Creation properties...

        /// <summary>
        /// Get/Set Property for product repository.
        /// </summary>
        public GenericRepository<Product> ProductRepository
        {
            get
            {
                if (this._productRepository == null)
                    this._productRepository = new GenericRepository<Product>(_context);
                return _productRepository;
            }
        }

        /// <summary>
        /// Get/Set Property for user repository.
        /// </summary>
        public GenericRepository<User> UserRepository
        {
            get
            {
                if (this._userRepository == null)
                    this._userRepository = new GenericRepository<User>(_context);
                return _userRepository;
            }
        }

        /// <summary>
        /// Get/Set Property for token repository.
        /// </summary>
        public GenericRepository<Token> TokenRepository
        {
            get
            {
                if (this._tokenRepository == null)
                    this._tokenRepository = new GenericRepository<Token>(_context);
                return _tokenRepository;
            }
        }
        #endregion

        #region Public member methods...
        /// <summary>
        /// Save method.
        /// </summary>
        public void Save()
        {
            try
            {
                _context.SaveChanges();
            }
            catch (DbEntityValidationException e)
            {
                var outputLines = new List<string>();
                foreach (var eve in e.EntityValidationErrors)
                {
                    outputLines.Add(string.Format("{0}: Entity of type \"{1}\" 
                    in state \"{2}\" has the following validation errors:", 
                    DateTime.Now, eve.Entry.Entity.GetType().Name, eve.Entry.State));
                    foreach (var ve in eve.ValidationErrors)
                    {
                        outputLines.Add(string.Format("- Property: \"{0}\", 
                        Error: \"{1}\"", ve.PropertyName, ve.ErrorMessage));
                    }
                }
                System.IO.File.AppendAllLines(@"C:\errors.txt", outputLines);

                throw e;
            }
        }

        #endregion

        #region Implementing IDiosposable...

        #region private dispose variable declaration...
        private bool disposed = false; 
        #endregion

        /// <summary>
        /// Protected Virtual Dispose method
        /// </summary>
        /// <param name="disposing"></param>
        protected virtual void Dispose(bool disposing)
        {
            if (!this.disposed)
            {
                if (disposing)
                {
                    Debug.WriteLine("UnitOfWork is being disposed");
                    _context.Dispose();
                }
            }
            this.disposed = true;
        }

        /// <summary>
        /// Dispose method
        /// </summary>
        public void Dispose()
        {
            Dispose(true);
            GC.SuppressFinalize(this);
        } 
        #endregion
    }
}

所以,现在IUnitOfWork中定义的所有接口成员都在UnitOfWork类中实现了

public interface IUnitOfWork
    {
        #region Properties
        GenericRepository<Product> ProductRepository { get; }
        GenericRepository<User> UserRepository { get; }
        GenericRepository<Token> TokenRepository { get; } 
        #endregion
        
        #region Public methods
        /// <summary>
        /// Save method.
        /// </summary>
        void Save(); 
        #endregion
    }

这样做不会改变我们现有代码的功能,但我们也需要使用此接口更新业务服务。我们将把IUnitOfWork接口实例传递到服务构造函数中,而不是直接使用UnitOfWork类。

        private readonly IUnitOfWork _unitOfWork;

        public ProductServices(IUnitOfWork unitOfWork)
        {
            _unitOfWork = unitOfWork;
        }

因此,我们的用户服务、令牌服务和产品服务的构造函数变为如下所示。

产品服务

用户服务

令牌服务

测试业务服务

我们将开始为BusinessServices项目编写单元测试。

步骤 1:测试项目

在现有Visual Studio中添加一个简单的类库,并将其命名为BusinessServices.Tests。打开工具->库包管理器->包管理器控制台以打开包管理器控制台窗口。我们需要在继续之前安装一些包。

步骤 2:安装NUnit包

在包管理器控制台中,选择BusinessServices.Tests作为默认项目,并输入命令"Install-Package NUnit –Version 2.6.4"。如果您不指定版本,PMC(包管理器控制台)将尝试下载最新版本的NUnit NuGet包,但我们特别需要2.6.4,因此我们需要指定版本。当您尝试从PMC安装任何此类包时,情况也相同。

安装成功后,您可以在项目引用中看到DLL引用,即nunit.framework

步骤 3:安装Moq框架

以与步骤2中解释的类似方式在同一项目上安装框架。输入命令"Install-Package Moq"。这里,我们使用最新版本的Moq。

因此,添加DLL

步骤 4:安装Entity Framework

Install-Package EntityFramework –Version 5.0.0

步骤 5:安装AutoMapper

Install-Package AutoMapper –Version 3.3.1

我们的package.config,即自动添加到项目中的文件,如下所示

<?xml version="1.0" encoding="utf-8"?>
<packages>
  <package id="AutoMapper" 
  version="3.3.1" targetFramework="net40" />
  <package id="EntityFramework" 
  version="5.0.0" targetFramework="net40" />
  <package id="Moq" 
  version="4.2.1510.2205" targetFramework="net40" />
  <package id="NUnit" 
  version="2.6.4" targetFramework="net40" />
</packages>

步骤 6:引用

DataModelBusinessServicesBusinessEntities项目的引用添加到此项目。

TestHelper

我们将需要一些辅助文件,这些文件将在BusinessServices.Tests项目和我们稍后将创建的WebAPI.Tests项目中使用。为了放置所有辅助文件,我又创建了一个名为TestHelper的类库项目。只需右键单击解决方案并添加一个名为TestHelper的新项目,并在其中添加一个名为DataInitializer.cs的类。此类包含三个简单的获取方法,即UserProductToken的虚拟数据。您可以将以下代码用作类的实现

using System;
using System.Collections.Generic;
using DataModel;

namespace TestsHelper
{
/// <summary>
/// Data initializer for unit tests
/// </summary>
public class DataInitializer
{
/// <summary>
/// Dummy products
/// </summary>
/// <returns></returns>
public static List<Product> GetAllProducts()
{
var products = new List<Product>
{
new Product() {ProductName = "Laptop"},
new Product() {ProductName = "Mobile"},
new Product() {ProductName = "HardDrive"},
new Product() {ProductName = "IPhone"},
new Product() {ProductName = "IPad"}
};
return products;
}

/// <summary>
/// Dummy tokens
/// </summary>
/// <returns></returns>
public static List<Token> GetAllTokens()
{
var tokens = new List<Token>
{
new Token()
{
AuthToken = "9f907bdf-f6de-425d-be5b-b4852eb77761",
ExpiresOn = DateTime.Now.AddHours(2),
IssuedOn = DateTime.Now,
UserId = 1
},
new Token()
{
AuthToken = "9f907bdf-f6de-425d-be5b-b4852eb77762",
ExpiresOn = DateTime.Now.AddHours(1),
IssuedOn = DateTime.Now,
UserId = 2
}
};

return tokens;
}

/// <summary>
/// Dummy users
/// </summary>
/// <returns></returns>
public static List<User> GetAllUsers()
{
var users = new List<User>
{
new User()
{
UserName = "akhil",
Password = "akhil",
Name = "Akhil Mittal",
},
new User()
{
UserName = "arsh",
Password = "arsh",
Name = "Arsh Mittal",
},
new User()
{
UserName = "divit",
Password = "divit",
Name = "Divit Agarwal",
}
};

return users;
}

}
}

在上面的类中,GetAllUsers()获取用户的虚拟数据,GetAllProducts()获取产品的虚拟数据,GetAllTokens()方法获取令牌的虚拟数据。所以现在,我们的解决方案有两个新项目,如下图所示

DataModel项目引用添加到TestHelper项目,并将TestHelper项目引用添加到BusinessServices.Tests项目。

ProductService 测试

我们将从设置项目和测试的先决条件开始,然后逐步进行实际测试。

测试设置

我们将继续创建ProductServices测试。在BusinessServices.Tests项目中添加一个名为ProductServicesTests.cs的新类。

声明变量

定义我们将在类中用于编写测试的private变量

        #region Variables
        private IProductServices _productService;
        private IUnitOfWork _unitOfWork;
        private List<Product> _products;
        private GenericRepository<Product> _productRepository;
        private WebApiDbEntities _dbEntities;
        #endregion

变量声明是自解释的,其中_productService将持有ProductServices的模拟,_unitOfWork用于UnitOfWork类,_products将持有来自TestHelper项目DataInitializer类的虚拟产品,_productRepository_dbEntities分别持有来自DataModel项目的Product Repository和WebAPIDbEntities的模拟。

编写测试夹具设置 (Test Fixture Setup)

测试夹具设置是所有测试的一次性设置。它在类的概念上类似于构造函数。当我们开始执行设置时,这是第一个执行的方法。在此方法中,我们将填充虚拟产品数据,并在方法顶部使用[TestFixtureSetUp]属性进行修饰,该属性告诉编译器该特定方法是一个TestFixtureSetup。[TestFixtureSetUp]属性是NUnit框架的一部分,因此将其作为命名空间包含在类中,即使用NUnit.Framework;。以下是TestFixtureSetup的代码。

       #region Test fixture setup

        /// <summary>
        /// Initial setup for tests
        /// </summary>
        [TestFixtureSetUp]
        public void Setup()
        {
            _products = SetUpProducts();
        }

        #endregion

       private static List<Product> SetUpProducts()
        {
            var prodId = new int();
            var products = DataInitializer.GetAllProducts();
            foreach (Product prod in products)
                prod.ProductId = ++prodId;
            return products;
        }

SetUpproducts()方法从DataInitializer类而不是数据库获取产品。它还通过迭代为每个产品分配唯一的ID。结果数据被分配给_products列表,用于设置模拟仓储以及在每个单独的测试中比较实际输出与结果输出。

编写测试夹具拆卸 (Test Fixture Tear Down)

TestFixtureSetup不同,拆卸用于解除分配或处置对象。它也仅在所有测试执行结束时执行一次。在我们的例子中,我们将使用此方法将_products实例置空。用于测试夹具拆卸的属性是[TestFixtureTearDown]。

以下是拆卸的代码

       #region TestFixture TearDown.

        /// <summary>
        /// TestFixture teardown
        /// </summary>
        [TestFixtureTearDown]
        public void DisposeAllObjects()
        {
            _products = null;
        }

        #endregion

请注意,到目前为止我们还没有编写任何单元测试。

编写测试设置 (Test Setup)

TestFixtureSetUp是一次性运行的过程,而带有[SetUp]标记的方法在每次测试之后执行。每个测试都应该是独立的,并且应该使用一组新的输入进行测试。Setup帮助我们为每个测试重新初始化数据。因此,所有测试所需的初始化都写在这个用[SetUp]属性标记的特定方法中。我编写了一些方法并在此方法中初始化了private变量。这些代码行在每个测试结束后执行,以便单个测试不依赖于任何其他已编写的测试,并且不会受到其他测试通过或失败状态的影响。Setup的代码如下

        #region Setup
        /// <summary>
        /// Re-initializes test.
        /// </summary>
        [SetUp]
        public void ReInitializeTest()
        {
            _dbEntities = new Mock<WebApiDbEntities>().Object;
            _productRepository = SetUpProductRepository();
            var unitOfWork = new Mock<IUnitOfWork>();
            unitOfWork.SetupGet(s => s.ProductRepository).Returns(_productRepository);
            _unitOfWork = unitOfWork.Object;
            _productService = new ProductServices(_unitOfWork);
        }

        #endregion

我们在此方法中利用Mock框架来模拟private变量实例。例如对于_dbEntities,我们编写_dbEntities = new Mock<WebApiDbEntities>().Object;。这意味着我们正在模拟WebDbEntities类并获取其代理对象。Mock类是Moq框架中的类,因此在类中包含相应的命名空间using Moq;。

编写测试拆卸 (Test Tear down)

与测试一样,Setup在每次测试后运行。类似地,Test [TearDown]在每次测试执行完成后被调用。您可以使用tear down来处理和置空在setup期间初始化的对象。tear down的方法应使用[TearDown]属性进行装饰。以下是测试tear down的实现。

        /// <summary>
        /// Tears down each test data
        /// </summary>
        [TearDown]
        public void DisposeTest()
        {
            _productService = null;
            _unitOfWork = null;
            _productRepository = null;
            if (_dbEntities != null)
                _dbEntities.Dispose();
        }
模拟仓储 (Mocking Repository)

我提到了模拟实体仓储。我创建了一个方法SetUpProductRepository()来模拟产品仓储,并在ReInitializeTest()方法中将其分配给_productrepository

private GenericRepository<Product> SetUpProductRepository()
{
// Initialise repository
var mockRepo = new Mock<GenericRepository<Product>>(MockBehavior.Default, _dbEntities);

// Setup mocking behavior
mockRepo.Setup(p => p.GetAll()).Returns(_products);

mockRepo.Setup(p => p.GetByID(It.IsAny<int>()))
.Returns(new Func<int, Product>(
id => _products.Find(p => p.ProductId.Equals(id))));

mockRepo.Setup(p => p.Insert((It.IsAny<Product>())))
.Callback(new Action<Product>(newProduct =>
{
dynamic maxProductID = _products.Last().ProductId;
dynamic nextProductID = maxProductID + 1;
newProduct.ProductId = nextProductID;
_products.Add(newProduct);
}));

mockRepo.Setup(p => p.Update(It.IsAny<Product>()))
.Callback(new Action<Product>(prod =>
{
var oldProduct = _products.Find(a => a.ProductId == prod.ProductId);
oldProduct = prod;
}));

mockRepo.Setup(p => p.Delete(It.IsAny<Product>()))
.Callback(new Action<Product>(prod =>
{
var productToRemove =
_products.Find(a => a.ProductId == prod.ProductId);

if (productToRemove != null)
_products.Remove(productToRemove);
}));

// Return mock implementation object
return mockRepo.Object;
}

在这里,我们模拟了产品仓储的所有必要方法,以从_products对象而不是实际数据库中获取所需数据。

这行代码

var mockRepo = new Mock<GenericRepository<Product>>(MockBehavior.Default, _dbEntities);

模拟了Product的通用仓储,mockRepo.Setup()通过将相关委托传递给方法来模拟仓储方法。

初始化工作单元 (UnitOfWork) 和服务 (Service)

我在ReInitializeTest()方法(即我们的设置方法)中编写了以下几行代码

var unitOfWork = new Mock<IUnitOfWork>();
unitOfWork.SetupGet(s => s.ProductRepository).Returns(_productRepository);
_unitOfWork = unitOfWork.Object;
_productService = new ProductServices(_unitOfWork);

在这里,您可以看到我正在尝试模拟UnitOfWork实例,并强制它在先前模拟的_productRepository上执行其所有事务和操作。这意味着所有事务都将局限于模拟的仓储,而实际数据库或实际仓储将不会被触及。服务也是如此;我们正在使用这个模拟的_unitOfWork初始化产品服务。因此,当我们在实际测试中使用_productService时,它实际上只作用于模拟的UnitOfWork和测试数据。

现在一切就绪,我们准备为ProductService编写单元测试。我们将编写测试来执行ProductService的所有CRUD操作。

1. GetAllProductsTest ()

我们的BusinessServices项目中的ProductService包含一个名为GetAllProducts ()的方法,其实现如下

public IEnumerable<BusinessEntities.ProductEntity> GetAllProducts()
{
var products = _unitOfWork.ProductRepository.GetAll().ToList();
if (products.Any())
{
Mapper.CreateMap<Product, ProductEntity>();
var productsModel = Mapper.Map<List<Product>, List<ProductEntity>>(products);
return productsModel;
}
return null;
}

我们在这里看到,此方法从数据库中获取所有可用产品,将数据库实体映射到我们的自定义BusinessEntities.ProductEntity,并返回自定义BusinessEntities.ProductEntity列表。如果未找到产品,则返回null

要开始编写测试方法,您需要使用NUnit框架的[Test]属性来修饰该测试方法。此属性指定特定方法是单元测试方法。

以下是我为上述业务服务方法编写的单元测试方法

[Test]
public void GetAllProductsTest()
{
var products = _productService.GetAllProducts();
var productList =
products.Select(
productEntity =>
new Product {ProductId = productEntity.ProductId, 
             ProductName = productEntity.ProductName}).ToList();
var comparer = new ProductComparer();
CollectionAssert.AreEqual(
productList.OrderBy(product => product, comparer),
_products.OrderBy(product => product, comparer), comparer);
}

我们使用了_productService的实例并调用了GetAllProducts()方法,该方法最终将在模拟的UnitOfWork和仓储上执行,以从_products列表中获取测试数据。该方法返回的产品类型为BusinessEntities.ProductEntity,我们需要将返回的产品与我们现有的_products列表(即DataModel.Product列表,一个模拟的数据库实体)进行比较,因此我们需要将返回的BusinessEntities.ProductEntity列表转换为DataModel.Product列表。我们通过以下代码行完成此操作

var productList =
products.Select(
productEntity =>
new Product {ProductId = productEntity.ProductId, 
             ProductName = productEntity.ProductName}).ToList();

现在我们有两个列表要比较,一个是_products列表,即实际的products,另一个是productList,即从服务返回的products。我编写了一个辅助类和比较方法,将两个Product列表转换到TestHelper项目中。此方法检查列表项并比较它们的值是否相等。您可以将一个名为ProductComparer的类添加到TestHelper项目,其实现如下

public class ProductComparer : IComparer, IComparer<product>
{
public int Compare(object expected, object actual)
{
var lhs = expected as Product;
var rhs = actual as Product;
if (lhs == null || rhs == null) throw new InvalidOperationException();
return Compare(lhs, rhs);
}

public int Compare(Product expected, Product actual)
{
int temp;
return (temp = expected.ProductId.CompareTo(actual.ProductId)) != 0 ? 
        temp : expected.ProductName.CompareTo(actual.ProductName);
}
}

为了断言结果,我们使用NUnit的CollectionAssert.AreEqual,其中我们传入两个列表和比较器。

CollectionAssert.AreEqual(
productList.OrderBy(product => product, comparer),
_products.OrderBy(product => product, comparer), comparer);

由于我的Visual Studio中安装了ReSharper提供的NUnit插件,我来调试一下测试方法,看看Assert的实际结果。我们将在文章的最后使用NUnit UI运行所有测试。

产品列表:

_产品:

我们得到了两个列表,我们需要检查列表的比较,所以我只需按F5并在TestUI上得到了结果,如下所示

这表明我们的测试已经通过,即预期结果和返回结果相同。

2. GetAllProductsTestForNull ()

你也可以为同一个方法的null检查编写测试,在该方法中,你在调用service方法之前将_products列表置为空。我们实际上需要编写覆盖被调用方法所有退出点的测试。

以下测试涵盖了该方法的另一个退出点,即在未找到产品的情况下返回null

        /// <summary>
        /// Service should return null
        /// </summary>
        [Test]
        public void GetAllProductsTestForNull()
        {
            _products.Clear();
            var products = _productService.GetAllProducts();
            Assert.Null(products);
            SetUpProducts();
        }

在上述测试中,我们首先清空了_products列表并调用了服务方法。现在对结果进行null断言,因为我们的预期结果和实际结果都应该为null。我再次调用了SetUpProducts()方法来填充_products列表,但您也可以在测试设置方法中完成此操作,即ReInitializeTest()

现在让我们转向其他测试。

3. GetProductByRightIdTest ()

在这里,我们测试ProductServiceGetProductById()方法。理想的行为是,如果我使用有效ID调用该方法,该方法应该返回有效产品。现在假设我知道我的产品"Mobile"的产品ID,我使用该ID调用测试,因此理想情况下我应该得到一个产品名称为“mobile”的产品。

        /// <summary>
        /// Service should return product if correct id is supplied
        /// </summary>
        [Test]
        public void GetProductByRightIdTest()
        {
            var mobileProduct = _productService.GetProductById(2);
            if (mobileProduct != null)
            {
                Mapper.CreateMap<ProductEntity, Product>();
                var productModel = Mapper.Map<ProductEntity, Product>(mobileProduct);
                AssertObjects.PropertyValuesAreEquals(productModel,
                                                      _products.Find(a => 
                                                      a.ProductName.Contains("Mobile")));
            }
        }

上述代码是自解释的,除了行AssertObjects.PropertyValuesAreEquals

_productService.GetProductById(2);行获取产品ID为2的产品。

Mapper.CreateMap<ProductEntity, Product>();
var productModel = Mapper.Map<ProductEntity, Product>(mobileProduct);

上述代码将返回的自定义ProductEntity映射到DataModel.Product

AssertObjects是我在TestHelper类中添加的另一个类。该类的目的是比较两个对象的属性。这是一个适用于所有具有属性的类对象的通用类。它的方法PropertyValuesAreEquals()检查属性是否相等。

断言对象类

using System.Collections;
using System.Reflection;
using NUnit.Framework;

namespace TestsHelper
{
    public static class AssertObjects
    {
        public static void PropertyValuesAreEquals(object actual, object expected)
        {
            PropertyInfo[] properties = expected.GetType().GetProperties();
            foreach (PropertyInfo property in properties)
            {
                object expectedValue = property.GetValue(expected, null);
                object actualValue = property.GetValue(actual, null);

                if (actualValue is IList)
                AssertListsAreEquals(property, (IList)actualValue, (IList)expectedValue);
                else if (!Equals(expectedValue, actualValue))
                if (property.DeclaringType != null)
                Assert.Fail("Property {0}.{1} does not match. 
                Expected: {2} but was: {3}", property.DeclaringType.Name, 
                property.Name, expectedValue, actualValue);
            }
        }

        private static void AssertListsAreEquals
        (PropertyInfo property, IList actualList, IList expectedList)
        {
        if (actualList.Count != expectedList.Count)
            Assert.Fail("Property {0}.{1} does not match. 
            Expected IList containing {2} elements but was IList containing {3} elements", 
            property.PropertyType.Name,
            property.Name, expectedList.Count, actualList.Count);

            for (int i = 0; i < actualList.Count; i++)
            if (!Equals(actualList[i], expectedList[i]))
            Assert.Fail("Property {0}.{1} does not match. Expected IList with element {1} 
            equals to {2} but was IList with element {1} equals to {3}", 
            property.PropertyType.Name, property.Name, expectedList[i], actualList[i]);
        }
    }
}

运行测试

4. GetProductByWrongIdTest ()

在此测试中,我们使用错误的ID测试服务方法,并期望返回null

        /// <summary>
        /// Service should return null
        /// </summary>
        [Test]
        public void GetProductByWrongIdTest()
        {
            var product = _productService.GetProductById(0);
            Assert.Null(product);
        }

5. AddNewProductTest ()

在此单元测试中,我们测试ProductServiceCreateProduct()方法。以下是为创建新产品而编写的单元测试。

/// <summary>
/// Add new product test
/// </summary>
[Test]
public void AddNewProductTest()
{
    var newProduct = new ProductEntity()
    {
    ProductName = "Android Phone"
    };

    var maxProductIDBeforeAdd = _products.Max(a => a.ProductId);
    newProduct.ProductId = maxProductIDBeforeAdd + 1;
    _productService.CreateProduct(newProduct);
    var addedproduct = new Product() 
    {ProductName = newProduct.ProductName, ProductId = newProduct.ProductId};
    AssertObjects.PropertyValuesAreEquals(addedproduct, _products.Last());
    Assert.That(maxProductIDBeforeAdd + 1, Is.EqualTo(_products.Last().ProductId));
}

在上述代码中,我创建了一个名为"Android Phone"的虚拟产品,并将产品ID指定为_products列表中产品productId最大值的递增ID。理想情况下,如果我的测试成功,添加的产品应反映在_products列表中,作为具有最大产品ID的最后一个product。为了验证结果,我使用了两个断言。第一个检查预期产品和实际产品的属性,第二个验证产品ID。

var addedproduct = new Product() 
    {ProductName = newProduct.ProductName, ProductId = newProduct.ProductId};

addedProduct是预期添加到_products列表中的自定义产品,而_products.Last()给我们列表中的最后一个product。因此,

AssertObjects.PropertyValuesAreEquals(addedproduct, _products.Last());

检查虚拟产品和最后添加产品的所有属性,并且,

Assert.That(maxProductIDBeforeAdd + 1, Is.EqualTo(_products.Last().ProductId));

检查最后添加的产品是否与创建产品时提供的产品ID相同。

完全执行后

测试通过,这意味着预期值,即产品ID 6,等于_products列表中最后添加产品的产品ID。我们还可以看到,之前_products列表中只有五个产品,现在我们又添加了第六个产品。

6. UpdateProductTest ()

这是一个单元测试,用于检查产品是否已更新。此测试针对ProductServiceUpdateProduct()方法。

/// <summary>
/// Update product test
/// </summary>
[Test]
public void UpdateProductTest()
{
    var firstProduct = _products.First();
    firstProduct.ProductName = "Laptop updated";
    var updatedProduct = new ProductEntity()
    {ProductName = firstProduct.ProductName, ProductId = firstProduct.ProductId};
    _productService.UpdateProduct(firstProduct.ProductId, updatedProduct);
    Assert.That(firstProduct.ProductId, Is.EqualTo(1)); // hasn't changed
    Assert.That(firstProduct.ProductName, 
                Is.EqualTo("Laptop updated")); // Product name changed
}

在此测试中,我尝试更新_products列表中的第一个产品。我将产品名称更改为“Laptop Updated”,并调用了ProductServiceUpdateProduct()方法。我进行了两个断言,以检查_products列表中更新后的产品,一个针对productId,另一个针对产品名称。我们看到,在断言时我们得到了更新后的产品。

7. DeleteProductTest ()

以下是ProductServiceDeleteProduct ()方法的测试。

        /// <summary>
        /// Delete product test
        /// </summary>
        [Test]
        public void DeleteProductTest()
        {
            int maxID = _products.Max(a => a.ProductId); // Before removal
            var lastProduct = _products.Last();

            // Remove last Product
            _productService.DeleteProduct(lastProduct.ProductId);
            Assert.That(maxID, Is.GreaterThan
                       (_products.Max(a => a.ProductId)));   // Max id reduced by 1
        }

我编写了一个测试来验证产品列表中产品的最大ID。获取产品的最大ID,删除最后一个产品,然后检查产品列表中产品的最大ID。之前的最大ID应该大于最后一个产品的产品ID。

删除前的最大ID是5,删除后是4,这意味着一个产品已从_products列表中删除,因此语句:Assert.That(maxID, Is.GreaterThan(_products.Max(a => a.ProductId)));通过,因为5大于4。

我们已在单元测试中涵盖了ProductService的所有方法。以下是涵盖此服务所有测试的最终类。

#region using namespaces.
using System;
using System.Collections.Generic;
using System.Linq;
using AutoMapper;
using BusinessEntities;
using DataModel;
using DataModel.GenericRepository;
using DataModel.UnitOfWork;
using Moq;
using NUnit.Framework;
using TestsHelper;

#endregion

namespace BusinessServices.Tests
{
    /// <summary>
    /// Product Service Test
    /// </summary>
    public class ProductServicesTest
    {
        #region Variables

        private IProductServices _productService;
        private IUnitOfWork _unitOfWork;
        private List<Product> _products;
        private GenericRepository<Product> _productRepository;
        private WebApiDbEntities _dbEntities;
        #endregion

        #region Test fixture setup

        /// <summary>
        /// Initial setup for tests
        /// </summary>
        [TestFixtureSetUp]
        public void Setup()
        {
            _products = SetUpProducts();
        }

        #endregion

        #region Setup

        /// <summary>
        /// Re-initializes test.
        /// </summary>
        [SetUp]
        public void ReInitializeTest()
        {
            _dbEntities = new Mock<WebApiDbEntities>().Object;
            _productRepository = SetUpProductRepository();
            var unitOfWork = new Mock<IUnitOfWork>();
            unitOfWork.SetupGet(s => s.ProductRepository).Returns(_productRepository);
            _unitOfWork = unitOfWork.Object;
            _productService = new ProductServices(_unitOfWork);
        }

        #endregion

        #region Private member methods

        /// <summary>
        /// Setup dummy repository
        /// </summary>
        /// <returns></returns>
        private GenericRepository<Product> SetUpProductRepository()
        {
            // Initialise repository
            var mockRepo = new Mock<GenericRepository<Product>>
                           (MockBehavior.Default, _dbEntities);

            // Setup mocking behavior
            mockRepo.Setup(p => p.GetAll()).Returns(_products);

            mockRepo.Setup(p => p.GetByID(It.IsAny<int>()))
            .Returns(new Func<int, Product>(
            id => _products.Find(p => p.ProductId.Equals(id))));

            mockRepo.Setup(p => p.Insert((It.IsAny<Product>())))
            .Callback(new Action<Product>(newProduct =>
            {
                dynamic maxProductID = _products.Last().ProductId;
                dynamic nextProductID = maxProductID + 1;
                newProduct.ProductId = nextProductID;
                _products.Add(newProduct);
            }));

            mockRepo.Setup(p => p.Update(It.IsAny<Product>()))
            .Callback(new Action<Product>(prod =>
            {
                var oldProduct = _products.Find(a => a.ProductId == prod.ProductId);
                oldProduct = prod;
            }));

            mockRepo.Setup(p => p.Delete(It.IsAny<Product>()))
            .Callback(new Action<Product>(prod =>
            {
                var productToRemove =
                _products.Find(a => a.ProductId == prod.ProductId);

                if (productToRemove != null)
                    _products.Remove(productToRemove);
            }));

            // Return mock implementation object
            return mockRepo.Object;
        }

        /// <summary>
        /// Setup dummy products data
        /// </summary>
        /// <returns></returns>
        private static List<Product> SetUpProducts()
        {
            var prodId = new int();
            var products = DataInitializer.GetAllProducts();
            foreach (Product prod in products)
                prod.ProductId = ++prodId;
            return products;
        }

        #endregion

        #region Unit Tests

        /// <summary>
        /// Service should return all the products
        /// </summary>
        [Test]
        public void GetAllProductsTest()
        {
            var products = _productService.GetAllProducts();
            if (products != null)
            {
                var productList =
                products.Select(
                productEntity =>
                new Product { ProductId = productEntity.ProductId, 
                              ProductName = productEntity.ProductName }).
                ToList();
                var comparer = new ProductComparer();
                CollectionAssert.AreEqual(
                productList.OrderBy(product => product, comparer),
                _products.OrderBy(product => product, comparer), comparer);
            }
        }

        /// <summary>
        /// Service should return null
        /// </summary>
        [Test]
        public void GetAllProductsTestForNull()
        {
            _products.Clear();
            var products = _productService.GetAllProducts();
            Assert.Null(products);
            SetUpProducts();
        }

        /// <summary>
        /// Service should return product if correct id is supplied
        /// </summary>
        [Test]
        public void GetProductByRightIdTest()
        {
            var mobileProduct = _productService.GetProductById(2);
            if (mobileProduct != null)
            {
                Mapper.CreateMap<ProductEntity, Product>();
                var productModel = Mapper.Map<ProductEntity, Product>(mobileProduct);
                AssertObjects.PropertyValuesAreEquals(productModel,
                _products.Find(a => a.ProductName.Contains("Mobile")));
            }
        }

        /// <summary>
        /// Service should return null
        /// </summary>
        [Test]
        public void GetProductByWrongIdTest()
        {
            var product = _productService.GetProductById(0);
            Assert.Null(product);
        }

        /// <summary>
        /// Add new product test
        /// </summary>
        [Test]
        public void AddNewProductTest()
        {
            var newProduct = new ProductEntity()
            {
                ProductName = "Android Phone"
            };

            var maxProductIDBeforeAdd = _products.Max(a => a.ProductId);
            newProduct.ProductId = maxProductIDBeforeAdd + 1;
            _productService.CreateProduct(newProduct);
            var addedproduct = new Product() 
            { ProductName = newProduct.ProductName, ProductId = newProduct.ProductId };
            AssertObjects.PropertyValuesAreEquals(addedproduct, _products.Last());
            Assert.That(maxProductIDBeforeAdd + 1, Is.EqualTo(_products.Last().ProductId));
        }

        /// <summary>
        /// Update product test
        /// </summary>
        [Test]
        public void UpdateProductTest()
        {
            var firstProduct = _products.First();
            firstProduct.ProductName = "Laptop updated";
            var updatedProduct = new ProductEntity() 
            { ProductName = firstProduct.ProductName, ProductId = firstProduct.ProductId };
            _productService.UpdateProduct(firstProduct.ProductId, updatedProduct);
            Assert.That(firstProduct.ProductId, Is.EqualTo(1)); // hasn't changed
            Assert.That(firstProduct.ProductName, 
            Is.EqualTo("Laptop updated")); // Product name changed
        }

        /// <summary>
        /// Delete product test
        /// </summary>
        [Test]
        public void DeleteProductTest()
        {
            int maxID = _products.Max(a => a.ProductId); // Before removal
            var lastProduct = _products.Last();

            // Remove last Product
            _productService.DeleteProduct(lastProduct.ProductId);
            Assert.That(maxID, Is.GreaterThan
            (_products.Max(a => a.ProductId))); // Max id reduced by 1
        }

        #endregion

        #region Tear Down

        /// <summary>
        /// Tears down each test data
        /// </summary>
        [TearDown]
        public void DisposeTest()
        {
            _productService = null;
            _unitOfWork = null;
            _productRepository = null;
            if (_dbEntities != null)
                _dbEntities.Dispose();
        }

        #endregion

        #region TestFixture TearDown.

        /// <summary>
        /// TestFixture teardown
        /// </summary>
        [TestFixtureTearDown]
        public void DisposeAllObjects()
        {
            _products = null;
        }

        #endregion
    }
}

TokenService 测试

既然我们已经完成了ProductService的所有测试,我相信您一定对如何编写方法的单元测试有了概念。请注意,单元测试主要只针对公开方法编写,因为private方法通过类中的那些public方法会自动得到测试。对于TokenService测试,我不会解释太多理论,只通过代码进行导航。我将在必要时解释细节。

测试设置

BusinessServices.Tests项目中添加一个名为TokenServicesTests.cs的新类。

声明变量

定义我们将在类中用于编写测试的private变量

        #region Variables
        private ITokenServices _tokenServices;
        private IUnitOfWork _unitOfWork;
        private List<Token> _tokens;
        private GenericRepository<Token> _tokenRepository;
        private WebApiDbEntities _dbEntities;
        private const string SampleAuthToken = "9f907bdf-f6de-425d-be5b-b4852eb77761";
        #endregion

在这里,_tokenService将持有TokenServices的模拟,_unitOfWork用于UnitOfWork类,__tokens将持有来自TestHelper项目DataInitializer类的虚拟令牌,_tokenRepository_dbEntities分别持有来自DataModel项目的Token Repository和WebAPIDbEntities的模拟。

编写测试夹具设置 (Test Fixture Setup)
        #region Test fixture setup

        /// <summary>
        /// Initial setup for tests
        /// </summary>
        [TestFixtureSetUp]
        public void Setup()
        {
            _tokens = SetUpTokens();
        }

        #endregion

SetUpTokens ()方法从DataInitializer类而不是数据库获取令牌,并通过迭代为每个令牌分配唯一的ID。

        /// <summary>
        /// Setup dummy tokens data
        /// </summary>
        /// <returns></returns>
        private static List<Token> SetUpTokens()
        {
            var tokId = new int();
            var tokens = DataInitializer.GetAllTokens();
            foreach (Token tok in tokens)
                tok.TokenId = ++tokId;
            return tokens;
        }

结果数据被分配给__tokens列表,用于设置模拟仓储以及在每个单独的测试中比较实际输出与结果输出。

编写测试夹具拆卸 (Test Fixture Tear Down)
        #region TestFixture TearDown.

        /// <summary>
        /// TestFixture teardown
        /// </summary>
        [TestFixtureTearDown]
        public void DisposeAllObjects()
        {
            _tokens = null;
        }

        #endregion
编写测试设置 (Test Setup)
#region Setup

    /// <summary>
    /// Re-initializes test.
    /// </summary>
    [SetUp]
    public void ReInitializeTest()
    {
        _dbEntities = new Mock<WebApiDbEntities>().Object;
        _tokenRepository = SetUpTokenRepository();
        var unitOfWork = new Mock<IUnitOfWork>();
        unitOfWork.SetupGet(s => s.TokenRepository).Returns(_tokenRepository);
        _unitOfWork = unitOfWork.Object;
        _tokenServices = new TokenServices(_unitOfWork);
    }

#endregion
编写测试拆卸 (Test Tear down)
        #region Tear Down

        /// <summary>
        /// Tears down each test data
        /// </summary>
        [TearDown]
        public void DisposeTest()
        {
            _tokenServices = null;
            _unitOfWork = null;
            _tokenRepository = null;
            if (_dbEntities != null)
                _dbEntities.Dispose();
        }

        #endregion
模拟仓储 (Mocking Repository)
private GenericRepository<Token> SetUpTokenRepository()
{
// Initialise repository
var mockRepo = new Mock<GenericRepository<Token>>(MockBehavior.Default, _dbEntities);

// Setup mocking behavior
mockRepo.Setup(p => p.GetAll()).Returns(_tokens);

mockRepo.Setup(p => p.GetByID(It.IsAny<int>()))
.Returns(new Func<int, Token>(
id => _tokens.Find(p => p.TokenId.Equals(id))));

mockRepo.Setup(p => p.GetByID(It.IsAny<string>()))
.Returns(new Func<string, Token>(
authToken => _tokens.Find(p => p.AuthToken.Equals(authToken))));

mockRepo.Setup(p => p.Insert((It.IsAny<Token>())))
.Callback(new Action<Token>(newToken =>
{
dynamic maxTokenID = _tokens.Last().TokenId;
dynamic nextTokenID = maxTokenID + 1;
newToken.TokenId = nextTokenID;
_tokens.Add(newToken);
}));

mockRepo.Setup(p => p.Update(It.IsAny<Token>()))
.Callback(new Action<Token>(token =>
{
var oldToken = _tokens.Find(a => a.TokenId == token.TokenId);
oldToken = token;
}));

mockRepo.Setup(p => p.Delete(It.IsAny<Token>()))
.Callback(new Action<Token>(prod =>
{
var tokenToRemove =
_tokens.Find(a => a.TokenId == prod.TokenId);

if (tokenToRemove != null)
_tokens.Remove(tokenToRemove);
}));
//Create setup for other methods too. note non virtual methods can not be set up

// Return mock implementation object
return mockRepo.Object;
}

注意,在模拟仓储时,我为GetById()设置了两个模拟。我在数据库中做了一个小改动,也将AuthToken字段标记为主键。因此,可能会出现模拟在调用方法时对哪个主键发出了请求感到困惑的情况。所以我为TokenIdAuthToken字段都实现了模拟

mockRepo.Setup(p => p.GetByID(It.IsAny<int>())).Returns(new Func<int, Token>(
id => _tokens.Find(p => p.TokenId.Equals(id))));

mockRepo.Setup(p => p.GetByID(It.IsAny<string>())).Returns(new Func<string, Token>(
authToken => _tokens.Find(p => p.AuthToken.Equals(authToken))));

整体设置与我们为ProductService编写的性质相同。让我们继续进行单元测试。

1. GenerateTokenByUseridTest ()

此单元测试用于测试TokenServices业务服务的GenerateToken方法。在此方法中,为用户在数据库中生成一个新令牌。我们将使用_tokens列表进行所有这些事务。目前,我们只有来自DataInitializer生成的_tokens列表中的两个令牌条目。现在,当测试执行时,它应该期望列表中再添加一个令牌。

       [Test]
        public void GenerateTokenByUserIdTest()
        {
            const int userId = 1;
            var maxTokenIdBeforeAdd = _tokens.Max(a => a.TokenId);
            var tokenEntity = _tokenServices.GenerateToken(userId);
            var newTokenDataModel = new Token()
                                        {
                                            AuthToken = tokenEntity.AuthToken,
                                            TokenId = maxTokenIdBeforeAdd+1,
                                            ExpiresOn = tokenEntity.ExpiresOn,
                                            IssuedOn = tokenEntity.IssuedOn,
                                            UserId = tokenEntity.UserId
                                        };
            AssertObjects.PropertyValuesAreEquals(newTokenDataModel, _tokens.Last());
        }

我将默认用户 ID 设为1,并从令牌列表中存储了最大令牌 ID。调用服务方法GenerateTokenEntity()。由于我们的服务方法返回BusinessEntities.TokenEntity,我们需要将其映射到新的DataModel.Token对象进行比较。因此,预期结果是此令牌的所有属性都应与_token列表的最后一个令牌匹配,假设该列表已通过测试更新。

现在,由于结果对象和实际对象的所有属性都匹配,所以我们的测试通过。

2. ValidateTokenWithRightAuthToken ()

        /// <summary>
        /// Validate token test
        /// </summary>
        [Test]
        public void ValidateTokenWithRightAuthToken()
        {
            var authToken = Convert.ToString(SampleAuthToken);
            var validationResult = _tokenServices.ValidateToken(authToken);
            Assert.That(validationResult,Is.EqualTo(true));
        }

此测试通过TokenServiceValidateToken方法验证AuthToken。理想情况下,如果传入正确的令牌,服务应返回true

这里,我们得到validationResulttrue,因此测试应该通过。

3. ValidateTokenWithWrongAuthToken ()

测试相同方法的替代退出点,因此,使用错误的令牌,服务应返回false

       [Test]
        public void ValidateTokenWithWrongAuthToken()
        {
            var authToken = Convert.ToString("xyz");
            var validationResult = _tokenServices.ValidateToken(authToken);
            Assert.That(validationResult, Is.EqualTo(false));
        }

这里validationResultfalse,并且与false值进行比较,所以测试理论上应该通过。

UserService 测试

我尝试根据我们的服务实现为UserService编写单元测试,但在模拟仓储的Get()方法时遇到了错误,该方法将谓词或where条件作为参数。

   public TEntity Get(Func<TEntity, Boolean> where)
        {
            return DbSet.Where(where).FirstOrDefault<TEntity>();
        }

我们的服务方法严重依赖Get方法,因此无法测试任何方法,但除此之外,您可以寻找任何其他能够处理这些情况的模拟框架。我猜这是模拟框架中的一个错误。或者,避免使用带有谓词的Get方法(我不建议这种方法,因为它违背了测试策略。我们的测试不应局限于方法的技术可行性)。我在模拟仓储时遇到了以下错误

“在非虚函数(在VB中可覆盖)上的无效设置”。我已经注释掉了所有UserService单元测试代码,您可以在提供的源代码中找到它。

通过NUnit UI测试

我们已经完成了几乎所有的BusinessServices测试,现在让我们尝试在NUnit UI上执行这些测试。

  1. 步骤 1

    启动NUnit UI。我已解释过如何在Windows机器上安装NUnit。只需使用其启动图标启动NUnit界面即可

  2. 第二步

    界面打开后,点击文件->新建项目,将项目命名为WebAPI.nunit并将其保存到任何Windows位置。

  3. 步骤 3

    现在,点击项目->添加程序集,然后浏览BusinessServices.Tests.dll(编译后为您的单元测试项目创建的库)。

  4. 步骤 4

    一旦浏览了程序集,您会看到该测试项目的所有单元测试都加载到UI中,并在界面上可见。

  5. 步骤 5

    在界面的右侧面板上,您将看到一个“运行”按钮,该按钮将运行业务服务的所有测试。只需选择左侧测试树中的BusinessServices节点,然后单击右侧的“运行”按钮。

    运行测试后,您会在右侧看到一个绿色进度条,左侧所有测试上会显示勾号。这意味着所有测试都已通过。如果任何测试失败,该测试上会显示一个叉号,右侧会显示一个红色进度条。

    但在这里,我们所有的测试都通过了。

WebAPI 测试

WebAPI的单元测试与服务方法不完全相同,但在测试HttpResponse、返回的JSON、异常响应等方面有所不同。请注意,我们将在WebAPI单元测试中以与服务类似的方式模拟类和仓储。测试Web API的一种方法是通过Web客户端和测试服务的实际端点或托管URL,但这不被视为单元测试,而是集成测试。在本文的下一部分,我将逐步解释Web API的单元测试过程。我们将为产品控制器编写测试。

结论

在本文中,我们学习了如何为核心业务逻辑编写单元测试,主要关注基本的CRUD操作。目的是了解单元测试是如何编写和执行的基本概念。您可以添加自己的特色,以帮助您在实际项目中。我的下一篇文章将解释WebAPI控制器的单元测试,这将是本文的延续。希望这篇文章对您有所帮助。您可以从GitHub下载包含包的本文完整源代码。祝您编程愉快!:)

其他系列

我的其他系列文章

历史记录

  • 2016年3月1日:初始版本
© . All rights reserved.