RESTful日#7:使用NUnit和Moq框架在WebAPI中进行单元测试和集成测试(第一部分)
表的内容 表内容介绍路线图单元测试nunit moq框架设置解决方案测试业务服务 步骤1:测试项目步骤2:安装NUnit包步骤3:安装Moq框架步骤4:安装实体框架步骤5:安装automapper步骤6:引用testhelper productservice测试 测试设置 声明变量 写测试装置设置写测试装置拆卸写测试装置写测试装置拆卸模拟库初始化单元工作和服务 getproductbywrongidtest () addnewproducttest () updateproductttest () deleteproducttest () TokenService测试 测试设置 声明变量写测试装置设置写测试装置拆下写测试装置写测试装置拆下模拟库 validatetokenwithrightauthtoken () validatetokenwithwrongauthtoken () UserService测试webapi测试 结论 介绍 我们在WebAPI中学到了很多东西。我们几乎涵盖了使用ASP构建一个健壮的完整堆栈REST服务所需的所有技术。NET WebAPI,从创建一个服务到使它成为一个安全的、随时可以使用的企业级应用程序样板。在本文中,我们将学习如何关注测试驱动开发,并为我们的服务端点和业务逻辑编写单元测试。我将使用NUnit和Moq框架为业务逻辑层和控制器方法编写测试用例。我将较少地介绍理论,而更多地关注如何使用这些框架来编写单元测试的实际实现。我把这篇文章分成了两部分。第一部分重点测试在我们的代码库中作为BusinessServices创建的业务逻辑和类库。第二部分将着重于测试Web API。种族隔离的目的很简单;这篇文章的范围是非常大的,可能会变成一个非常大的帖子,这将不容易在一次阅读。 路线图 下面是我逐步学习WebAPI的路线图: RESTful日#1:企业级应用程序架构,使用实体框架、通用存储库模式和工作单元的Web api。RESTful日#2:使用Unity容器和引导程序在Web api中使用依赖注入实现控制反转。RESTful日#3:使用Unity容器和可管理扩展框架(MEF)在Asp.net Web api中使用控制反转和依赖注入来解决依赖关系的依赖关系。RESTful日#4:使用MVC 4 Web api中的属性路由自定义URL重写/路由。RESTful日#5:使用操作过滤器的Web api中基于基本身份验证和令牌的自定义授权。RESTful日#6:使用操作过滤器、异常过滤器和NLog在Web api中进行请求日志记录和异常处理/日志记录。RESTful日#7:使用NUnit和Moq框架在WebAPI中进行单元测试和集成测试(第一部分)。使用NUnit和Moq框架在WebAPI中进行单元测试和集成测试(第二部分)。净Web api。RESTful日#10:创建自托管的ASP。NET WebAPI与CRUD操作在Visual Studio 2010 我有意使用Visual Studio 2010和。net Framework 4.0,因为在。net Framework 4.0中很少有很难找到的实现,但是我将通过演示如何实现来简化它。 单元测试 单元测试允许您快速地对代码进行重大更改。您知道它现在可以工作,因为您已经运行了测试,当您进行需要进行的更改时,您需要让测试再次工作。这可以节省大量的时间。”这是我从stack overflow上的一篇文章中得到的,我完全同意这个说法。 一个好的单元测试可以帮助开发人员理解他的代码和(最重要的)业务逻辑。单元测试有助于理解业务逻辑的所有方面,从所需的输入和输出到代码可能失败的条件。如果单元测试覆盖了执行所需的所有测试用例,那么编写良好的单元测试的代码失败的机会就更小。 NUnit 有各种各样的框架可用来进行单元测试。NUnit是我比较喜欢的。NUnit与。net很好地结合在一起,并且为编写单元测试提供了灵活性。它具有有意义且自解释的属性和类名,可以帮助开发人员以一种简单的方式编写测试。NUnit提供了一个易于使用的交互式GUI,您可以在其中运行测试并获得详细信息。它以一种漂亮的方式显示通过或失败的测试的数量,如果有任何测试失败,它还提供堆栈跟踪,从而使您能够在GUI本身执行第一级调试。我建议在你的machi上下载并安装NUnitne表示运行测试。在编写完所有测试之后,我们将使用NUnit GUI。我通常使用内置的图形用户界面的NUnit由ReSharper集成在我的Visual Studio。然而,我建议您使用NUnit GUI来运行测试,因为ReSharper是一个付费的库,并且只有少数开发人员可以集成它。因为我们使用的是Visual Studio 2010,所以我们需要使用旧版本的NUnit,也就是2.6.4。您可以下载并运行.msi,并按照此URL安装到您的机器上。 一旦你完成安装,你会看到NUnit已经安装在你的机器上,如下图所示: Moq的框架 Moq是一个简单而直接的库,可以模拟c#中的对象。我们可以在模拟库的帮助下模拟数据、存储库、类和实例。因此,当我们编写单元测试时,我们不会在实际的类实例上执行它们,而是通过创建类对象的代理来执行内存中的单元测试。与NUnit一样,Moq库类也易于使用和理解。它的几乎所有方法、类和接口名称都是自解释的。 下面是从Wikipedia上获取的关于为什么使用模拟对象的列表 对象提供不确定的结果(例如,当前时间或当前温度);具有不容易创建或再现的状态(例如,网络错误);速度慢(例如,一个完整的数据库,必须在测试前初始化);尚不存在或可能改变行为的;必须包含专门用于测试目的(而不是用于实际任务)的信息和方法。 所以无论我们写什么测试,我们实际上是在测试数据和代理对象上执行的,也就是说,不是在真实类的实例上。我们将使用Moq来模拟数据和存储库,这样我们就不会为了执行单元测试而一次又一次地访问数据库。你可以在这篇文章中了解更多关于Moq的信息。 设置解决方案 我将使用这篇文章来解释如何为业务逻辑编写单元测试,即覆盖我们的业务逻辑层和WebAPI控制器。单元测试的范围不应该仅仅局限于业务逻辑或端点,还应该扩展到所有公开的逻辑,比如过滤器和处理程序。编写良好的单元测试应该涵盖几乎所有的代码。您可以通过在线提供的一些工具来跟踪代码覆盖率。我们将不测试过滤器和公共类,但将关注控制器和业务逻辑层,并了解如何继续进行单元测试。我将使用本系列第6天之前使用的相同源代码,并将继续使用本系列上一篇文章中获得的最新代码库。代码库可以从本文下载。当你从我上一篇文章中取出代码库并在Visual Studio中打开它时,你会看到项目结构如下图所示: IUnitOfWork是我添加的新接口,以方便接口驱动开发。它有助于模拟对象,改进结构和可读性。只需打开Visual Studio,在DataModel项目的UnitOfWork文件夹下添加一个名为IUnitOfWork的新界面,并定义UnitOfWork类中使用的属性,如下所示: 现在,转到UnitOfWork类并使用这个接口继承这个类,因此UnitOfWork类就变成了这样的东西 隐藏,收缩,复制Code
#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> /// <paramname="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类中实现: 隐藏,复制Code
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类。 隐藏,复制Code
private readonly IUnitOfWork _unitOfWork; public ProductServices(IUnitOfWork unitOfWork) { _unitOfWork = unitOfWork; }
因此,我们的用户服务、令牌服务和产品服务构造函数如下所示, 产品服务 用户服务 令牌服务 测试业务服务 我们将开始为BusinessServices项目编写单元测试。 步骤1:测试项目 在现有的Visual Studio中添加一个简单的类库,并将其命名为BusinessServices.Tests。打开Tools->Library包管理器->包管理器控制台以打开包管理器控制台窗口。在我们继续之前,我们需要安装软件包。 步骤2:安装NUnit包 在包管理器控制台中,选择BusinessServices。作为默认项目进行测试,并编写命令“Install-Package NUnit -Version 2.6.4”。如果你没有提到版本,PMC(包管理控制台)会尝试下载最新版本的NUnit nugget包,但是我们特别需要2.6.4,所以我们需要提到版本。同样适用于尝试从PMC安装任何此类包 安装成功后,您可以在项目引用中看到DLL引用,即nunit.framework, 步骤3:安装Moq框架 按照步骤2中解释的类似方法在同一个项目上安装框架。写命令”安装包这里我们使用的是最新版本的最小起订量 因此添加DLL 步骤4:安装实体框架 安装包EntityFramework版本5.0.0 步骤5:安装自动程序 安装包自动程序-版本3.3.1 我们的包。配置,即自动添加到项目中看起来像, 隐藏,复制Code
<?xmlversion="1.0"encoding="utf-8"?> <packages> <packageid="AutoMapper"version="3.3.1"targetFramework="net40"/> <packageid="EntityFramework"version="5.0.0"targetFramework="net40"/> <packageid="Moq"version="4.2.1510.2205"targetFramework="net40"/> <packageid="NUnit"version="2.6.4"targetFramework="net40"/> </packages>
第六步:引用 向此项目添加DataModel、BusinessServices、BusinessEntities项目的引用。 TestHelper 我们将只需要BusinessServices中所需要的少量助手文件。测试项目和我们的WebAPI。测试我们稍后将创建的项目。为了放置所有的助手文件,我创建了一个名为TestHelper的类库项目。只需右键单击该解决方案,并添加名为TestHelper的新项目,并将名为DataInitializer.cs的类添加到其中。这个类包含三个获取虚拟数据的简单方法,即用户的、产品的和令牌的虚拟数据。您可以使用以下代码作为类实现: 隐藏,收缩,复制Code
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()为产品获取虚拟数据,getalltoken()方法为令牌获取虚拟数据。所以现在,我们的解决方案有两个新项目,如下所示: 将DataModel项目引用添加到TestHelper项目,并将TestHelper项目引用添加到BusinessServices。测试项目。 ProductService测试 我们将从设置项目和设置测试的先决条件开始,然后逐步转移到实际测试。 测试设置 我们将继续创建ProductServices测试。在BusinessServices中添加一个名为ProductServicesTests.cs的新类。测试项目。 声明变量 定义我们将在类中用于编写测试的私有变量, 隐藏,复制Code
#region Variables private IProductServices _productService; private IUnitOfWork _unitOfWork; private List<Product> _products; private GenericRepository<Product> _productRepository; private WebApiDbEntities _dbEntities; #endregion
变量声明是自解释的,_productService将模拟ProductServices, _unitOfWork UnitOfWork类,商品将虚拟产品DataInitializer TestHelper项目类,_productRepository和_dbEntities模拟产品库和WebAPIDbEntities分别从DataModel项目。 编写测试设备设置 测试夹具设置是为所有测试编写的一次性设置。就类而言,它类似于构造函数。当我们开始执行setup时,这是要执行的第一个方法。在这个方法中,我们将填充虚拟产品数据,并在顶部用[TestFixtureSetUp]属性装饰该方法,该属性告诉编译器特定的方法是TestFixtureSetUp。[TestFixtureSetUp]属性是NUnit框架的一部分,所以在类中包含它作为命名空间,例如使用NUnit. framework;下面是TestFixtureSetup的代码。 隐藏,复制Code
#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; }
方法从DataInitializer类而不是从database中获取产品。它还通过迭代每个产品为它们分配一个惟一的id。结果数据被分配给_products列表,用于设置模拟存储库和在每个测试中比较实际输出和结果输出。 编写测试夹具拆卸 与TestFixtureSetup不同,tear down用于分配或释放对象。当所有测试执行结束时,它也只执行一次。在本例中,我们将使用这个方法来nullify _products实例。用于测试夹具拆卸的属性是[TestFixtureTearDown]。 下面是用于拆卸的代码。 隐藏,复制Code
#region TestFixture TearDown. /// <summary> /// TestFixture teardown /// </summary> [TestFixtureTearDown] public void DisposeAllObjects() { _products = null; } #endregion
请注意,到目前为止我们还没有编写任何单元测试。 写测试设置 TestFixtureSetUp是一次运行的进程,而[SetUp]标记的方法是在每次测试之后执行。每个测试都应该是独立的,并且应该用一组新的输入进行测试。Setup帮助我们为每个测试重新初始化数据。因此,测试所需的所有初始化都在这个用[SetUp]属性标记的方法中编写。我已经编写了一些方法并在这个方法中初始化了私有变量。这些代码行在每个测试结束后执行,因此单个测试不依赖于任何其他编写的测试,也不受其他测试通过或失败状态的影响。设置代码: 隐藏,复制Code
#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
我们在这个方法中使用模拟框架来模拟私有变量实例。与_dbEntities类似,我们写_dbEntities = new Mock<WebApiDbEntities>(). object;。这意味着我们正在模拟WebDbEntities类并获取它的代理对象。Mock类是来自Moq框架的类,因此使用Moq包含相应的名称空间;在课堂上 编写测试分解 与测试一样,安装程序在每次测试之后运行。类似地,在每次测试执行完成后调用Test [TearDown]。可以使用拆毁来释放在设置时初始化的对象并使其无效。拆除的方法应该用[TearDown]属性装饰。下面是测试拆除实现。 隐藏,复制Code
/// <summary> /// Tears down each test data /// </summary> [TearDown] public void DisposeTest() { _productService = null; _unitOfWork = null; _productRepository = null; if (_dbEntities != null) _dbEntities.Dispose(); }
嘲笑库 我讲过了嘲弄回购实体的位置。我创建了一个方法SetUpProductRepository()来模拟产品存储库,并将其分配给ReInitializeTest()方法中的_productrepository。 隐藏,收缩,复制Code
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; }
在这里,我们模拟Product Repository所需的所有方法,以便从_products对象而不是从实际的数据库获取所需的数据。 一行代码 隐藏,复制Code
var mockRepo = new Mock<GenericRepository<Product>>(MockBehavior.Default, _dbEntities);
模拟产品的通用存储库,mockreep . setup()通过向该方法传递相关的委托来模拟存储库方法。 初始化单元工作和服务 我在ReInitializeTest()方法中编写了以下代码,即我们的setup方法, 隐藏,复制Code
var unitOfWork = new Mock<IUnitOfWork>(); unitOfWork.SetupGet(s => s.ProductRepository).Returns(_productRepository); _unitOfWork = unitOfWork.Object; _productService = new ProductServices(_unitOfWork);
在这里,您可以看到我试图模拟UnitOfWork实例,并强制它在前面模拟的_productRepository上执行它的所有事务和操作。这意味着所有的事务将被限制在模拟的存储库中,而实际的数据库或实际的存储库将不会被触及。服务也是如此;我们用这个mock _unitOfWork初始化产品服务。因此,当我们在实际测试中使用_productService时,它实际上只对模拟的UnitOfWork和测试数据有效。 现在一切就绪,我们可以为ProductService编写单元测试了。我们将编写测试来执行作为ProductService一部分的所有CRUD操作。 1. GetAllProductsTest () 我们的BusinessServices项目中的ProductService包含一个名为GetAllProducts()的方法,下面是实现, 隐藏,复制Code
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。并返回定制的BusinessEntities.ProductEntity列表。如果没有找到产品,则返回null。 要开始编写测试方法,您需要使用NUnit框架的[test]属性来装饰该测试方法。此属性指定特定的方法是单元测试方法。 以下是我针对上述业务服务方法编写的单元测试方法: 隐藏,复制Code
[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。我们需要将返回的产品与现有的_products列表(比如DataModel列表)进行比较。Product(模拟的数据库实体),因此我们需要转换返回的BusinessEntities。ProductEntity列表到数据模型。产品列表。我们用下面的代码行来做这个: 隐藏,复制Code
var productList = products.Select( productEntity => new Product {ProductId = productEntity.ProductId, ProductName = productEntity.ProductName}).ToList();
现在我们有两个列表要比较,一个是_products列表,即实际产品,另一个是productList,即从服务返回的产品。我编写了一个助手类和比较方法来转换TestHelper项目中的两个产品列表。此方法检查列表项并比较它们的值是否相等。你可以添加一个名为ProductComparer的类到TestHelper项目,实现如下: 隐藏,复制Code
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); } }</product>
要断言结果,我们使用CollectionAssert。AreEqual of NUnit在这里我们传递了列表和比较器。 隐藏,复制Code
CollectionAssert.AreEqual( productList.OrderBy(product => product, comparer), _products.OrderBy(product => product, comparer), comparer);
由于我的Visual Studio中有ReSharper提供的NUnit插件,让我调试测试方法来查看Assert的实际结果。在本文的最后,我们将使用NUnit UI运行所有测试。 productList: 商品: 我们得到了两个列表,我们需要检查列表的比较,所以我只按下F5,在TestUI上得到结果为: 这表明我们的测试通过了,即预期的结果和返回的结果是相同的。 2. GetAllProductsTestForNull () 您还可以为调用服务方法之前nullify _products列表的相同方法编写null检查测试。实际上,我们需要编写覆盖被调用方法的所有出口点的测试。 下面的测试覆盖了该方法的另一个出口点,如果没有找到产品,该出口点将返回null。 隐藏,复制Code
/// <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的产品。 隐藏,复制Code
/// <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);line获取产品id为2的产品。 隐藏,复制Code
Mapper.CreateMap<ProductEntity, Product>();
var productModel = Mapper.Map<ProductEntity, Product>(mobileProduct);
上面的代码将返回的自定义ProductEntity映射到DataModel.Product AssertObjects是我在TestHelper类中添加的另一个类。这个类的目的是比较两个对象的属性。这是一个通用的泛型类,适用于所有具有属性的类对象类型。它的方法PropertyValuesAreEquals()检查属性是否相等。 AssertObjects类 隐藏,收缩,复制Code
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。 隐藏,复制Code
/// <summary> /// Service should return null /// </summary> [Test] public void GetProductByWrongIdTest() { var product = _productService.GetProductById(0); Assert.Null(product); }
5. AddNewProductTest () 在这个单元测试中,我们测试ProductService的CreateProduct()方法。下面是为创建新产品而编写的单元测试。 隐藏,复制Code
/// <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作为递增id分配给product tid的最大值,该产品位于_products列表中。理想情况下,如果测试成功,添加的产品应该在_products列表中反映为具有最大产品id的最后一个产品。为了验证结果,我使用了两个断言。第一个检查预期和实际产品的属性,第二个检查产品id。 隐藏,复制Code
var addedproduct = new Product() {ProductName = newProduct.ProductName, ProductId = newProduct.ProductId};
addedProduct是期望添加到_products列表中的定制产品,而_products. last()为我们提供列表中的最后一个产品。所以, AssertObjects。PropertyValuesAreEquals (addedproduct _products.Last ());检查哑产品和最后添加产品的所有性能, 断言。(maxProductIDBeforeAdd + 1, Is.EqualTo(_products.Last().ProductId));检查最后添加的产品是否与创建产品时提供的产品id相同。 完整的执行后, 测试通过,这意味着产品id 6的期望值等于_products列表中最后添加的产品的产品id。我们还可以看到,之前我们在_products列表中只有5种产品,现在我们添加了第6种。 6. updateproduct () 这是检查产品是否更新的单元测试。此测试用于ProductService的UpdateProduct()方法。 隐藏,复制Code
/// <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()方法的测试。 隐藏,复制Code
/// <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 }
我已经写了测试,从产品列表中验证产品的max id。获取产品的最大id,删除最后一个产品,并从列表中检查产品的最大id。先前的最大id应该大于最后一个产品的产品id。 Max id在delete之前是5,delete之后是4,这意味着一个产品被从_products列表中删除,因此语句:Assert。(maxID Is.GreaterThan(商品。马克斯(=比;a.ProductId)));当5大于4时传递。 我们已经在单元测试中涵盖了ProductService的所有方法。下面是涵盖此服务的所有测试的最后一个类。 隐藏,收缩,复制Code
#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的所有测试,我相信您一定对如何为方法编写单元测试有了一些想法。请注意,单元测试主要只编写为公开的方法,因为私有方法会通过类中的公共方法自动进行测试。我不会解释太多关于TokenService测试的理论,只会浏览代码。我将在任何必要的地方解释细节。 测试设置 在BusinessServices中添加一个名为TokenServicesTests.cs的新类。测试项目。 声明变量 定义我们将在类中用于编写测试的私有变量, 隐藏,复制Code
#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的mock, _unitOfWork持有UnitOfWork类,其余的令牌将持有来自TestHelper项目的DataInitializer类的伪令牌,_tokenRepository和_dbEntities分别持有来自DataModel项目的令牌库和WebAPIDbEntities的mock令牌 编写测试设备设置 隐藏,复制Code
#region Test fixture setup /// <summary> /// Initial setup for tests /// </summary> [TestFixtureSetUp] public void Setup() { _tokens = SetUpTokens(); } #endregion
方法从DataInitializer类而不是从database中获取令牌,并通过迭代令牌为每个令牌分配一个惟一的id。 隐藏,复制Code
/// <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; }
结果数据被分配给剩余的令牌列表,用于设置模拟存储库和在每个单独的测试中比较实际输出和结果输出。 令状测试夹具拆卸 隐藏,复制Code
#region TestFixture TearDown. /// <summary> /// TestFixture teardown /// </summary> [TestFixtureTearDown] public void DisposeAllObjects() { _tokens = null; } #endregion
写测试设置 隐藏,复制Code
#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
编写测试分解 隐藏,复制Code
#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
嘲笑库 隐藏,收缩,复制Code
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 virtauls methods can not be set up // Return mock implementation object return mockRepo.Object; }
注意,在模拟库中,我为GetById()设置了两个模拟。我在数据库中做了一个小更改,我也将AuthToken字段标记为主键。因此,在调用针对哪个主键发出请求的方法时,mock可能会感到困惑。所以我已经实现了TokenId和AuthToken字段的模拟: 隐藏,复制Code
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 list。目前,我们在_tokens列表中只有两个由DataInitializer生成的令牌项。现在,当测试执行时,应该会有一个令牌被添加到列表中。 隐藏,复制Code
[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,我们需要将它映射到新的数据模型。标记对象进行比较。因此,预期的结果是,这个令牌的所有属性都应该匹配_token列表的最后一个令牌,假设该列表已通过测试更新。 现在,由于结果和实际对象的所有属性都匹配,所以测试通过了。 2. ValidateTokenWithRightAuthToken () 隐藏,复制Code
/// <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为真,因此测试应该通过。 3.ValidateTokenWithWrongAuthToken () 因此,用错误的令牌测试相同方法的备用出口点时,服务应该返回false。 隐藏,复制Code
[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条件作为参数。 隐藏,复制Code
public TEntity Get(Func<TEntity, Boolean> where) { return DbSet.Where(where).FirstOrDefault<TEntity>(); }
我们的服务方法严重依赖于Get方法,因此不能测试任何方法,但除此之外,您可以搜索处理这些情况的任何其他mocking框架。我猜这是mock框架的一个bug。另外,不要使用带有谓词的Get方法(我不建议使用这种方法,因为它违背了测试策略。我们的试验不应局限于方法在技术上的可行性。我得到以下错误,而模仿库: "在非虚拟(VB中可重写)上的设置无效"。我已经注释掉了所有UserService单元测试代码,你可以在可用的源代码中找到它。 通过NUnit UI进行测试 我们已经完成了几乎所有的BusinessServices测试,现在让我们尝试在NUnit UI上执行这些测试。 步骤1: 发射NUnit UI。我已经解释了如何在windows机器上安装NUnit。用它的启动图标启动NUnit接口 步骤2: 打开界面后,单击File ->新项目,并将项目命名为WebAPI。nunit,并将其保存在任何窗口位置。 步骤3: 现在,点击项目->添加程序集并浏览BusinessServices.Tests.dll(在编译时为单元测试项目创建的库) 步骤4: 浏览程序集之后,您将看到该测试项目的所有单元测试都在UI中加载,并且在界面上可见。 步骤5: 在界面的右侧面板中,您将看到一个Run按钮,用于运行business service的所有测试。只需在左侧的测试树中选择节点BusinessServices,并按下右侧的Run按钮。 运行测试后,您将在右侧看到绿色的进度条,在左侧的所有测试上都有标记。这意味着所有的测试都通过了。如果任何测试失败,您将在测试上得到十字标记和右侧红色的进度条。 但在这里,我们所有的测试都通过了。 之前的测试 WebAPI的单元测试与服务方法不完全一样,但在测试HttpResponse、返回的JSON、异常响应等方面有所不同。请注意,在WebAPI单元测试中,我们将以与服务类似的方式模拟类和存储库。测试web api的一种方法是通过web客户机测试服务的实际端点或托管URL,但这并不被视为单元测试,这被称为集成测试。在本文的下一部分中,我将逐步解释步骤p尝试对web API进行单元测试。我们将为Product Controller编写测试。 结论 在本文中,我们学习了如何为核心业务逻辑和主要针对基本CRUD操作编写单元测试。其目的是了解单元测试是如何编写和执行的。你可以添加自己的风格,这对你的实时项目有帮助。我的下一篇文章解释了WebAPI控制器的单元测试,它将是本部分的延续。我希望这对你们有用。您可以从GitHub下载本文的完整源代码和包。编码快乐:) 其他系列 我的其他系列文章: MVC架构和关注点分离简介:第1部分:面向对象(第1天):多态性和继承(早期绑定/编译时多态性) 本文转载于:http://www.diyabc.com/frontweb/news411.html