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






4.94/5 (38投票s)
在本文中,我们将学习如何为核心业务逻辑编写单元测试,主要关注基本的CRUD操作。
目录
- 引言
- 路线图
- 单元测试
- NUnit
- Moq 框架
- 设置解决方案
- 测试业务服务
- 步骤 1:测试项目
- 步骤 2:安装NUnit包
- 步骤 3:安装Moq框架
- 步骤 4:安装Entity Framework
- 步骤 5:安装AutoMapper
- 步骤 6:引用
- TestHelper
- ProductService 测试
- UserService 测试
- WebAPI 测试
- 结论
- 历史
介绍
我们一直在学习WebAPI。我们几乎涵盖了使用ASP.NET WebAPI构建健壮的全栈REST服务所需的所有技术,从创建服务到使其成为安全且随时可用的企业级应用程序样板。在本文中,我们将学习如何专注于测试驱动开发,并为我们的服务端点和业务逻辑编写单元测试。我将使用NUnit和Moq框架来为业务逻辑层和控制器方法编写测试用例。我将减少理论讲解,更多地关注如何使用这些框架编写单元测试的实际实现。我将文章分为两部分。第一部分侧重于测试业务逻辑和在我们代码库中创建为BusinessServices的类库。第二部分将侧重于测试Web API。分层的原因很简单;这篇文章的范围非常大,可能会变成一篇非常长的文章,难以一次性阅读完。
路线图
以下是我为逐步学习WebAPI设置的路线图
- RESTful Day #1:使用Entity Framework、通用仓储模式和工作单元的Web API企业级应用程序架构
- RESTful 日常 #2:在 Web API 中使用 Unity Container 和 Bootstrapper 通过依赖注入实现控制反转
- RESTful Day #3:在ASP.NET Web API中使用Unity Container和Managed Extensibility Framework (MEF) 解决依赖的依赖,实现控制反转和依赖注入
- RESTful Day #4:在MVC 4 Web API中使用属性路由自定义URL重写/路由
- RESTful Day #5:在Web API中使用动作过滤器实现基本认证和基于令牌的自定义授权
- RESTful Day #6:在Web API中使用动作过滤器、异常过滤器和NLog实现请求日志记录和异常处理/日志记录
- RESTful Day #7:在WebAPI中使用NUnit和Moq框架进行单元测试和集成测试(第一部分)
- RESTful Day #8:在WebAPI中使用NUnit和Moq框架进行单元测试和集成测试(第二部分)
- RESTful Day #9:在ASP.NET Web API中扩展OData支持
- RESTful Day #10:在Visual Studio 2010中创建带有CRUD操作的自托管ASP.NET 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:引用
将DataModel
、BusinessServices
、BusinessEntities
项目的引用添加到此项目。
TestHelper
我们将需要一些辅助文件,这些文件将在BusinessServices.Tests
项目和我们稍后将创建的WebAPI.Tests
项目中使用。为了放置所有辅助文件,我又创建了一个名为TestHelper
的类库项目。只需右键单击解决方案并添加一个名为TestHelper
的新项目,并在其中添加一个名为DataInitializer.cs的类。此类包含三个简单的获取方法,即User
、Product
和Token
的虚拟数据。您可以将以下代码用作类的实现
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 ()
在这里,我们测试ProductService
的GetProductById()
方法。理想的行为是,如果我使用有效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 ()
在此单元测试中,我们测试ProductService
的CreateProduct()
方法。以下是为创建新产品而编写的单元测试。
/// <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 ()
这是一个单元测试,用于检查产品是否已更新。此测试针对ProductService
的UpdateProduct()
方法。
/// <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”,并调用了ProductService
的UpdateProduct()
方法。我进行了两个断言,以检查_products
列表中更新后的产品,一个针对productId
,另一个针对产品名称。我们看到,在断言时我们得到了更新后的产品。
7. DeleteProductTest ()
以下是ProductService
中DeleteProduct ()
方法的测试。
/// <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
字段标记为主键。因此,可能会出现模拟在调用方法时对哪个主键发出了请求感到困惑的情况。所以我为TokenId
和AuthToken
字段都实现了模拟
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));
}
此测试通过TokenService
的ValidateToken
方法验证AuthToken
。理想情况下,如果传入正确的令牌,服务应返回true
。
这里,我们得到validationResult
为true
,因此测试应该通过。
3. ValidateTokenWithWrongAuthToken ()
测试相同方法的替代退出点,因此,使用错误的令牌,服务应返回false
。
[Test]
public void ValidateTokenWithWrongAuthToken()
{
var authToken = Convert.ToString("xyz");
var validationResult = _tokenServices.ValidateToken(authToken);
Assert.That(validationResult, Is.EqualTo(false));
}
这里validationResult
为false
,并且与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
启动NUnit UI。我已解释过如何在Windows机器上安装NUnit。只需使用其启动图标启动NUnit界面即可
- 第二步
界面打开后,点击文件->新建项目,将项目命名为
WebAPI.nunit
并将其保存到任何Windows位置。 - 步骤 3
现在,点击项目->添加程序集,然后浏览BusinessServices.Tests.dll(编译后为您的单元测试项目创建的库)。
- 步骤 4
一旦浏览了程序集,您会看到该测试项目的所有单元测试都加载到UI中,并在界面上可见。
- 步骤 5
在界面的右侧面板上,您将看到一个“运行”按钮,该按钮将运行业务服务的所有测试。只需选择左侧测试树中的
BusinessServices
节点,然后单击右侧的“运行”按钮。运行测试后,您会在右侧看到一个绿色进度条,左侧所有测试上会显示勾号。这意味着所有测试都已通过。如果任何测试失败,该测试上会显示一个叉号,右侧会显示一个红色进度条。
但在这里,我们所有的测试都通过了。
WebAPI 测试
WebAPI的单元测试与服务方法不完全相同,但在测试HttpResponse
、返回的JSON、异常响应等方面有所不同。请注意,我们将在WebAPI单元测试中以与服务类似的方式模拟类和仓储。测试Web API的一种方法是通过Web客户端和测试服务的实际端点或托管URL,但这不被视为单元测试,而是集成测试。在本文的下一部分,我将逐步解释Web API的单元测试过程。我们将为产品控制器编写测试。
结论
在本文中,我们学习了如何为核心业务逻辑编写单元测试,主要关注基本的CRUD操作。目的是了解单元测试是如何编写和执行的基本概念。您可以添加自己的特色,以帮助您在实际项目中。我的下一篇文章将解释WebAPI控制器的单元测试,这将是本文的延续。希望这篇文章对您有所帮助。您可以从GitHub下载包含包的本文完整源代码。祝您编程愉快!:)
其他系列
我的其他系列文章
历史记录
- 2016年3月1日:初始版本