本文共 57312 字,大约阅读时间需要 191 分钟。
目录
在ASP.NET Core 2.0中创建Web API,非常详细的操作步骤,包括单元测试和集成测试
让我们使用最新版本的ASP.NET Core和Entity Framework Core创建一个Web API。
在本指南中,我们将使用WideWorldImporters数据库来创建Web API。
REST API至少提供以下操作:
REST还有其他操作,但本指南不需要它们。
这些操作允许客户端通过REST API执行操作,因此我们的Web API必须包含这些操作。
WideWorldImporters 数据库包含4个模式:
在本指南中,我们将使用Warehouse.StockItems表格。我们将添加代码以使用此实体:允许检索库存项目,按ID检索库存项目,创建,更新和删除数据库中的库存项目。
此API的版本为1。
这是API的路由表:
Verb | Url | Description |
GET | api/v1/Warehouse/StockItem | 返回库存商品 |
GET | api/v1/Warehouse/StockItem/id | 更加id返回库存项目 |
POST | api/v1/Warehouse/StockItem | 新建一个新的库存项目 |
PUT | api/v1/Warehouse/StockItem/id | 更新已存在的库存项目 |
DELETE | api/v1/Warehouse/StockItem/id | 删除已存在的库存项目 |
请牢记这些路线,因为API必须实现所有路线。
对于本指南,源代码的工作目录是C:\ Projects。
打开Visual Studio并按照下列步骤操作:
在下一个窗口中,选择API和.ASP.NET Core的最新版本,在本例中为2.1:
Visual Studio完成解决方案创建后,我们将看到此窗口:
在此步骤中,我们需要安装以下NuGet包:
现在,我们将继续EntityFrameworkCore.SqlServer从Nuget 安装软件包,右键单击WideWorldImporters.API项目:
更改为“浏览”选项卡并键入Microsoft.EntityFrameworkCore.SqlServer:
接下来,安装Swashbuckle.AspNetCore包:
Swashbuckle.AspNetCore package允许为Web API启用帮助页面。
这是项目的结构。
现在运行项目以检查解决方案是否准备就绪,按F5,Visual Studio将显示此浏览器窗口:
默认情况下,Visual Studio ValuesController在Controllers目录中添加一个带有名称的文件,将其从项目中删除。
现在,使用名称Models创建一个目录并添加以下文件:
Entities.cs将包含与Entity Framework Core相关的所有代码。
Extensions.cs将包含DbContext和集合的扩展方法。
Requests.cs将包含请求的定义。
Responses.cs将包含响应的定义。
Entities.cs文件的代码:
using System;using Microsoft.EntityFrameworkCore;using Microsoft.EntityFrameworkCore.Metadata.Builders;namespace WideWorldImporters.API.Models{#pragma warning disable CS1591 public partial class StockItem { public StockItem() { } public StockItem(int? stockItemID) { StockItemID = stockItemID; } public int? StockItemID { get; set; } public string StockItemName { get; set; } public int? SupplierID { get; set; } public int? ColorID { get; set; } public int? UnitPackageID { get; set; } public int? OuterPackageID { get; set; } public string Brand { get; set; } public string Size { get; set; } public int? LeadTimeDays { get; set; } public int? QuantityPerOuter { get; set; } public bool? IsChillerStock { get; set; } public string Barcode { get; set; } public decimal? TaxRate { get; set; } public decimal? UnitPrice { get; set; } public decimal? RecommendedRetailPrice { get; set; } public decimal? TypicalWeightPerUnit { get; set; } public string MarketingComments { get; set; } public string InternalComments { get; set; } public string CustomFields { get; set; } public string Tags { get; set; } public string SearchDetails { get; set; } public int? LastEditedBy { get; set; } public DateTime? ValidFrom { get; set; } public DateTime? ValidTo { get; set; } } public class StockItemsConfiguration : IEntityTypeConfiguration{ public void Configure(EntityTypeBuilder builder) { // Set configuration for entity builder.ToTable("StockItems", "Warehouse"); // Set key for entity builder.HasKey(p => p.StockItemID); // Set configuration for columns builder.Property(p => p.StockItemName).HasColumnType("nvarchar(200)").IsRequired(); builder.Property(p => p.SupplierID).HasColumnType("int").IsRequired(); builder.Property(p => p.ColorID).HasColumnType("int"); builder.Property(p => p.UnitPackageID).HasColumnType("int").IsRequired(); builder.Property(p => p.OuterPackageID).HasColumnType("int").IsRequired(); builder.Property(p => p.Brand).HasColumnType("nvarchar(100)"); builder.Property(p => p.Size).HasColumnType("nvarchar(40)"); builder.Property(p => p.LeadTimeDays).HasColumnType("int").IsRequired(); builder.Property(p => p.QuantityPerOuter).HasColumnType("int").IsRequired(); builder.Property(p => p.IsChillerStock).HasColumnType("bit").IsRequired(); builder.Property(p => p.Barcode).HasColumnType("nvarchar(100)"); builder.Property(p => p.TaxRate).HasColumnType("decimal(18, 3)").IsRequired(); builder.Property(p => p.UnitPrice).HasColumnType("decimal(18, 2)").IsRequired(); builder.Property(p => p.RecommendedRetailPrice).HasColumnType("decimal(18, 2)"); builder.Property(p => p.TypicalWeightPerUnit).HasColumnType("decimal(18, 3)").IsRequired(); builder.Property(p => p.MarketingComments).HasColumnType("nvarchar(max)"); builder.Property(p => p.InternalComments).HasColumnType("nvarchar(max)"); builder.Property(p => p.CustomFields).HasColumnType("nvarchar(max)"); builder.Property(p => p.LastEditedBy).HasColumnType("int").IsRequired(); // Computed columns builder .Property(p => p.StockItemID) .HasColumnType("int") .IsRequired() .HasComputedColumnSql("NEXT VALUE FOR [Sequences].[StockItemID]"); builder .Property(p => p.Tags) .HasColumnType("nvarchar(max)") .HasComputedColumnSql("json_query([CustomFields],N'$.Tags')"); builder .Property(p => p.SearchDetails) .HasColumnType("nvarchar(max)") .IsRequired() .HasComputedColumnSql("concat([StockItemName],N' ',[MarketingComments])"); // Columns with generated value on add or update builder .Property(p => p.ValidFrom) .HasColumnType("datetime2") .IsRequired() .ValueGeneratedOnAddOrUpdate(); builder .Property(p => p.ValidTo) .HasColumnType("datetime2") .IsRequired() .ValueGeneratedOnAddOrUpdate(); } } public class WideWorldImportersDbContext : DbContext { public WideWorldImportersDbContext(DbContextOptions options) : base(options) { } protected override void OnModelCreating(ModelBuilder modelBuilder) { // Apply configurations for entity modelBuilder .ApplyConfiguration(new StockItemsConfiguration()); base.OnModelCreating(modelBuilder); } public DbSet StockItems { get; set; } }#pragma warning restore CS1591}
Code for Extensions.cs文件:
using System.Linq;using System.Threading.Tasks;using Microsoft.EntityFrameworkCore;namespace WideWorldImporters.API.Models{#pragma warning disable CS1591 public static class WideWorldImportersDbContextExtensions { public static IQueryableGetStockItems(this WideWorldImportersDbContext dbContext, int pageSize = 10, int pageNumber = 1, int? lastEditedBy = null, int? colorID = null, int? outerPackageID = null, int? supplierID = null, int? unitPackageID = null) { // Get query from DbSet var query = dbContext.StockItems.AsQueryable(); // Filter by: 'LastEditedBy' if (lastEditedBy.HasValue) query = query.Where(item => item.LastEditedBy == lastEditedBy); // Filter by: 'ColorID' if (colorID.HasValue) query = query.Where(item => item.ColorID == colorID); // Filter by: 'OuterPackageID' if (outerPackageID.HasValue) query = query.Where(item => item.OuterPackageID == outerPackageID); // Filter by: 'SupplierID' if (supplierID.HasValue) query = query.Where(item => item.SupplierID == supplierID); // Filter by: 'UnitPackageID' if (unitPackageID.HasValue) query = query.Where(item => item.UnitPackageID == unitPackageID); return query; } public static async Task GetStockItemsAsync(this WideWorldImportersDbContext dbContext, StockItem entity) => await dbContext.StockItems.FirstOrDefaultAsync(item => item.StockItemID == entity.StockItemID); public static async Task GetStockItemsByStockItemNameAsync(this WideWorldImportersDbContext dbContext, StockItem entity) => await dbContext.StockItems.FirstOrDefaultAsync(item => item.StockItemName == entity.StockItemName); } public static class IQueryableExtensions { public static IQueryable Paging (this IQueryable query, int pageSize = 0, int pageNumber = 0) where TModel : class => pageSize > 0 && pageNumber > 0 ? query.Skip((pageNumber - 1) * pageSize).Take(pageSize) : query; }#pragma warning restore CS1591}
Requests.cs文件的代码:
using System;using System.ComponentModel.DataAnnotations;namespace WideWorldImporters.API.Models{#pragma warning disable CS1591 public class PostStockItemsRequest { [Key] public int? StockItemID { get; set; } [Required] [StringLength(200)] public string StockItemName { get; set; } [Required] public int? SupplierID { get; set; } public int? ColorID { get; set; } [Required] public int? UnitPackageID { get; set; } [Required] public int? OuterPackageID { get; set; } [StringLength(100)] public string Brand { get; set; } [StringLength(40)] public string Size { get; set; } [Required] public int? LeadTimeDays { get; set; } [Required] public int? QuantityPerOuter { get; set; } [Required] public bool? IsChillerStock { get; set; } [StringLength(100)] public string Barcode { get; set; } [Required] public decimal? TaxRate { get; set; } [Required] public decimal? UnitPrice { get; set; } public decimal? RecommendedRetailPrice { get; set; } [Required] public decimal? TypicalWeightPerUnit { get; set; } public string MarketingComments { get; set; } public string InternalComments { get; set; } public string CustomFields { get; set; } public string Tags { get; set; } [Required] public string SearchDetails { get; set; } [Required] public int? LastEditedBy { get; set; } public DateTime? ValidFrom { get; set; } public DateTime? ValidTo { get; set; } } public class PutStockItemsRequest { [Required] [StringLength(200)] public string StockItemName { get; set; } [Required] public int? SupplierID { get; set; } public int? ColorID { get; set; } [Required] public decimal? UnitPrice { get; set; } } public static class Extensions { public static StockItem ToEntity(this PostStockItemsRequest request) => new StockItem { StockItemID = request.StockItemID, StockItemName = request.StockItemName, SupplierID = request.SupplierID, ColorID = request.ColorID, UnitPackageID = request.UnitPackageID, OuterPackageID = request.OuterPackageID, Brand = request.Brand, Size = request.Size, LeadTimeDays = request.LeadTimeDays, QuantityPerOuter = request.QuantityPerOuter, IsChillerStock = request.IsChillerStock, Barcode = request.Barcode, TaxRate = request.TaxRate, UnitPrice = request.UnitPrice, RecommendedRetailPrice = request.RecommendedRetailPrice, TypicalWeightPerUnit = request.TypicalWeightPerUnit, MarketingComments = request.MarketingComments, InternalComments = request.InternalComments, CustomFields = request.CustomFields, Tags = request.Tags, SearchDetails = request.SearchDetails, LastEditedBy = request.LastEditedBy, ValidFrom = request.ValidFrom, ValidTo = request.ValidTo }; }#pragma warning restore CS1591}
Responses.cs文件的代码:
using System.Collections.Generic;using System.Net;using Microsoft.AspNetCore.Mvc;namespace WideWorldImporters.API.Models{#pragma warning disable CS1591 public interface IResponse { string Message { get; set; } bool DidError { get; set; } string ErrorMessage { get; set; } } public interface ISingleResponse: IResponse { TModel Model { get; set; } } public interface IListResponse : IResponse { IEnumerable Model { get; set; } } public interface IPagedResponse : IListResponse { int ItemsCount { get; set; } double PageCount { get; } } public class Response : IResponse { public string Message { get; set; } public bool DidError { get; set; } public string ErrorMessage { get; set; } } public class SingleResponse : ISingleResponse { public string Message { get; set; } public bool DidError { get; set; } public string ErrorMessage { get; set; } public TModel Model { get; set; } } public class ListResponse : IListResponse { public string Message { get; set; } public bool DidError { get; set; } public string ErrorMessage { get; set; } public IEnumerable Model { get; set; } } public class PagedResponse : IPagedResponse { public string Message { get; set; } public bool DidError { get; set; } public string ErrorMessage { get; set; } public IEnumerable Model { get; set; } public int PageSize { get; set; } public int PageNumber { get; set; } public int ItemsCount { get; set; } public double PageCount => ItemsCount < PageSize ? 1 : (int)(((double)ItemsCount / PageSize) + 1); } public static class ResponseExtensions { public static IActionResult ToHttpResponse(this IResponse response) { var status = response.DidError ? HttpStatusCode.InternalServerError : HttpStatusCode.OK; return new ObjectResult(response) { StatusCode = (int)status }; } public static IActionResult ToHttpResponse (this ISingleResponse response) { var status = HttpStatusCode.OK; if (response.DidError) status = HttpStatusCode.InternalServerError; else if (response.Model == null) status = HttpStatusCode.NotFound; return new ObjectResult(response) { StatusCode = (int)status }; } public static IActionResult ToHttpResponse (this IListResponse response) { var status = HttpStatusCode.OK; if (response.DidError) status = HttpStatusCode.InternalServerError; else if (response.Model == null) status = HttpStatusCode.NoContent; return new ObjectResult(response) { StatusCode = (int)status }; } }#pragma warning restore CS1591}
了解模型
实体
StockItems类是Warehouse.StockItems表的表示。
StockItemsConfiguration类包含类的映射StockItems。
WideWorldImportersDbContext 类是数据库和C#代码之间的链接,这个类处理查询并提交数据库中的更改,当然还有另外一些事情。
扩展
WideWorldImportersDbContextExtensions 包含DbContext实例的扩展方法,一种方法用于检索stock items,另一种用于按ID检索stock item,最后一种用于按名称检索stock item。
IQueryableExtensions包含扩展方法IQueryable,用于添加分页功能。
要求
我们有以下定义:
PostStockItemsRequest 表示用于创建新stock item的模型,包含要保存在数据库中的所有必需属性。
PutStockItemsRequest代表机型更新现有stock item,在这种情况下只包含4个属性:StockItemName,SupplierID,ColorID和UnitPrice。此类不包含StockItemID属性,因为id位于控制器操作的路径中。
请求模型不需要包含实体等所有属性,因为我们不需要在请求或响应中暴露完整定义,使用具有少量属性的模型来限制数据是一种很好的做法。
Extensions类包含一个PostStockItemsRequest的扩展方法,用于StockItem从请求模型返回类的实例。
回复
这些是接口:
这些接口中的每一个都有实现,如果返回对象而不将它们封装在这些模型中更简单,为什么我们需要这些定义呢?请记住,这个Web API将为客户端提供操作,具有UI或没有UI,如果发生错误,它更容易拥有发送消息的属性,拥有模型或发送信息,此外,我们在响应中设置Http状态代码描述请求的结果。
这些类是通用的,因为通过这种方式,我们可以节省定义将来响应的时间,此Web API仅返回单个实体,列表和分页列表的响应。
ISingleResponse 表示对单个实体的响应。
IListResponse 表示带有列表的响应,例如,所有运送到现有订单而不进行分页。
IPagedResponse 表示具有分页的响应,例如日期范围内的所有订单。
ResponseExtensions类包含用于转换Http响应中的响应的扩展方法,这些方法在发生错误时返回InternalServerError(500)状态,OK(200)如果成功,如果数据库中不存在实体则返回NotFound(404),或者返回NoContent(204)用于列表响应没有模特。
现在,在Controllers目录内,添加名为WarehouseController.cs的代码文件并添加以下代码:
using System;using System.Threading.Tasks;using Microsoft.AspNetCore.Mvc;using Microsoft.EntityFrameworkCore;using Microsoft.Extensions.Logging;using WideWorldImporters.API.Models;namespace WideWorldImporters.API.Controllers{#pragma warning disable CS1591 [ApiController] [Route("api/v1/[controller]")] public class WarehouseController : ControllerBase { protected readonly ILogger Logger; protected readonly WideWorldImportersDbContext DbContext; public WarehouseController(ILoggerlogger, WideWorldImportersDbContext dbContext) { Logger = logger; DbContext = dbContext; }#pragma warning restore CS1591 // GET // api/v1/Warehouse/StockItem /// /// Retrieves stock items /// /// Page size /// Page number /// Last edit by (user id) /// Color id /// Outer package id /// Supplier id /// Unit package id ///A response with stock items list ///Returns the stock items list ///If there was an internal server error [HttpGet("StockItem")] [ProducesResponseType(200)] [ProducesResponseType(500)] public async TaskGetStockItemsAsync(int pageSize = 10, int pageNumber = 1, int? lastEditedBy = null, int? colorID = null, int? outerPackageID = null, int? supplierID = null, int? unitPackageID = null) { Logger?.LogDebug("'{0}' has been invoked", nameof(GetStockItemsAsync)); var response = new PagedResponse (); try { // Get the "proposed" query from repository var query = DbContext.GetStockItems(); // Set paging values response.PageSize = pageSize; response.PageNumber = pageNumber; // Get the total rows response.ItemsCount = await query.CountAsync(); // Get the specific page from database response.Model = await query.Paging(pageSize, pageNumber).ToListAsync(); response.Message = string.Format("Page {0} of {1}, Total of products: {2}.", pageNumber, response.PageCount, response.ItemsCount); Logger?.LogInformation("The stock items have been retrieved successfully."); } catch (Exception ex) { response.DidError = true; response.ErrorMessage = "There was an internal error, please contact to technical support."; Logger?.LogCritical("There was an error on '{0}' invocation: {1}", nameof(GetStockItemsAsync), ex); } return response.ToHttpResponse(); } // GET // api/v1/Warehouse/StockItem/5 /// /// Retrieves a stock item by ID /// /// Stock item id ///A response with stock item ///Returns the stock items list ///If stock item is not exists ///If there was an internal server error [HttpGet("StockItem/{id}")] [ProducesResponseType(200)] [ProducesResponseType(404)] [ProducesResponseType(500)] public async TaskGetStockItemAsync(int id) { Logger?.LogDebug("'{0}' has been invoked", nameof(GetStockItemAsync)); var response = new SingleResponse (); try { // Get the stock item by id response.Model = await DbContext.GetStockItemsAsync(new StockItem(id)); } catch (Exception ex) { response.DidError = true; response.ErrorMessage = "There was an internal error, please contact to technical support."; Logger?.LogCritical("There was an error on '{0}' invocation: {1}", nameof(GetStockItemAsync), ex); } return response.ToHttpResponse(); } // POST // api/v1/Warehouse/StockItem/ /// /// Creates a new stock item /// /// Request model ///A response with new stock item ///Returns the stock items list ///A response as creation of stock item ///For bad request ///If there was an internal server error [HttpPost("StockItem")] [ProducesResponseType(200)] [ProducesResponseType(201)] [ProducesResponseType(400)] [ProducesResponseType(500)] public async TaskPostStockItemAsync([FromBody]PostStockItemsRequest request) { Logger?.LogDebug("'{0}' has been invoked", nameof(PostStockItemAsync)); var response = new SingleResponse (); try { var existingEntity = await DbContext .GetStockItemsByStockItemNameAsync(new StockItem { StockItemName = request.StockItemName }); if (existingEntity != null) ModelState.AddModelError("StockItemName", "Stock item name already exists"); if (!ModelState.IsValid) return BadRequest(); // Create entity from request model var entity = request.ToEntity(); // Add entity to repository DbContext.Add(entity); // Save entity in database await DbContext.SaveChangesAsync(); // Set the entity to response model response.Model = entity; } catch (Exception ex) { response.DidError = true; response.ErrorMessage = "There was an internal error, please contact to technical support."; Logger?.LogCritical("There was an error on '{0}' invocation: {1}", nameof(PostStockItemAsync), ex); } return response.ToHttpResponse(); } // PUT // api/v1/Warehouse/StockItem/5 /// /// Updates an existing stock item /// /// Stock item ID /// Request model ///A response as update stock item result ///If stock item was updated successfully ///For bad request ///If there was an internal server error [HttpPut("StockItem/{id}")] [ProducesResponseType(200)] [ProducesResponseType(400)] [ProducesResponseType(500)] public async TaskPutStockItemAsync(int id, [FromBody]PutStockItemsRequest request) { Logger?.LogDebug("'{0}' has been invoked", nameof(PutStockItemAsync)); var response = new Response(); try { // Get stock item by id var entity = await DbContext.GetStockItemsAsync(new StockItem(id)); // Validate if entity exists if (entity == null) return NotFound(); // Set changes to entity entity.StockItemName = request.StockItemName; entity.SupplierID = request.SupplierID; entity.ColorID = request.ColorID; entity.UnitPrice = request.UnitPrice; // Update entity in repository DbContext.Update(entity); // Save entity in database await DbContext.SaveChangesAsync(); } catch (Exception ex) { response.DidError = true; response.ErrorMessage = "There was an internal error, please contact to technical support."; Logger?.LogCritical("There was an error on '{0}' invocation: {1}", nameof(PutStockItemAsync), ex); } return response.ToHttpResponse(); } // DELETE // api/v1/Warehouse/StockItem/5 /// /// Deletes an existing stock item /// /// Stock item ID ///A response as delete stock item result ///If stock item was deleted successfully ///If there was an internal server error [HttpDelete("StockItem/{id}")] [ProducesResponseType(200)] [ProducesResponseType(500)] public async TaskDeleteStockItemAsync(int id) { Logger?.LogDebug("'{0}' has been invoked", nameof(DeleteStockItemAsync)); var response = new Response(); try { // Get stock item by id var entity = await DbContext.GetStockItemsAsync(new StockItem(id)); // Validate if entity exists if (entity == null) return NotFound(); // Remove entity from repository DbContext.Remove(entity); // Delete entity in database await DbContext.SaveChangesAsync(); } catch (Exception ex) { response.DidError = true; response.ErrorMessage = "There was an internal error, please contact to technical support."; Logger?.LogCritical("There was an error on '{0}' invocation: {1}", nameof(DeleteStockItemAsync), ex); } return response.ToHttpResponse(); } }}
所有控制器操作的过程是:
请记住以Async后缀结尾的方法的所有名称,因为所有操作都是异步的,但在Http属性中,我们不使用此后缀。
ASP.NET Core能够原生方式依赖注入,这意味着我们不需要任何第三方框架在控制器注入依赖。
这是一个很大的挑战,因为我们需要从Web Forms和ASP.NET MVC改变主意,因为那些技术使用框架来注入依赖关系是一种奢侈,现在在ASP.NET Core依赖注入是一个基本方面。
ASP.NET Core的项目模板有一个带有名称Startup的类,在这个类中我们必须添加配置来为DbContext,Services,Loggers等注入实例。
修改Startup.cs文件的代码如下所示:
using System;using System.IO;using System.Reflection;using Microsoft.AspNetCore.Builder;using Microsoft.AspNetCore.Hosting;using Microsoft.AspNetCore.Mvc;using Microsoft.EntityFrameworkCore;using Microsoft.Extensions.Configuration;using Microsoft.Extensions.DependencyInjection;using Microsoft.Extensions.Logging;using Swashbuckle.AspNetCore.Swagger;using WideWorldImporters.API.Controllers;using WideWorldImporters.API.Models;namespace WideWorldImporters.API{#pragma warning disable CS1591 public class Startup { public Startup(IConfiguration configuration) { Configuration = configuration; } public IConfiguration Configuration { get; } // This method gets called by the runtime. Use this method to add services to the container. public void ConfigureServices(IServiceCollection services) { services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1); // Add configuration for DbContext // Use connection string from appsettings.json file services.AddDbContext(options => { options.UseSqlServer(Configuration["AppSettings:ConnectionString"]); }); // Set up dependency injection for controller's logger services.AddScoped >(); // Register the Swagger generator, defining 1 or more Swagger documents services.AddSwaggerGen(options => { options.SwaggerDoc("v1", new Info { Title = "WideWorldImporters API", Version = "v1" }); // Get xml comments path var xmlFile = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml"; var xmlPath = Path.Combine(AppContext.BaseDirectory, xmlFile); // Set xml path options.IncludeXmlComments(xmlPath); }); } // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. public void Configure(IApplicationBuilder app, IHostingEnvironment env) { if (env.IsDevelopment()) app.UseDeveloperExceptionPage(); // Enable middleware to serve generated Swagger as a JSON endpoint. app.UseSwagger(); // Enable middleware to serve swagger-ui (HTML, JS, CSS, etc.), specifying the Swagger JSON endpoint. app.UseSwaggerUI(c => { c.SwaggerEndpoint("/swagger/v1/swagger.json", "WideWorldImporters API V1"); }); app.UseMvc(); } }#pragma warning restore CS1591}
该ConfigureServices方法指定了如何解析依赖关系。我们需要建立DbContext和Logging。
该Configure方法为Http请求运行时添加了配置。
在运行Web API项目之前,在appsettings.json文件中添加连接字符串:
{ "Logging": { "LogLevel": { "Default": "Warning" } }, "AllowedHosts": "*", "AppSettings": { "ConnectionString": "server=(local);database=WideWorldImporters;integrated security=yes;" }}
要在帮助页面中显示说明,请为Web API项目启用XML文档:
现在,按F5开始调试Web API项目,如果一切正常,我们将在浏览器中获得以下输出:
另外,我们可以在另一个标签中加载帮助页面:
要为API项目添加单元测试,请按照下列步骤操作:
管理WideWorldImporters.API.UnitTests项目的引用:
现在添加WideWorldImporters.API项目的引用:
创建项目后,为项目添加以下NuGet包:
删除UnitTest1.cs文件。
保存更改并构建WideWorldImporters.API.UnitTests项目。
现在我们继续添加与单元测试相关的代码,这些测试将与内存数据库一起使用。
什么是TDD?测试是当今常见的做法,因为通过单元测试,在发布之前很容易对功能进行测试,测试驱动开发(TDD)是定义单元测试和验证代码行为的方法。
TDD的另一个概念是AAA:安排,行动和断言 ; Arrange是用于创建对象的代码块,Act是用于放置方法的所有调用的代码块,Assert是用于验证方法调用的结果的代码块。
由于我们正在使用内存数据库进行单元测试,因此我们需要创建一个类来模拟WideWorldImportersDbContext类,并添加数据以执行IWarehouseRepository操作测试。
需要明确的是:这些单元测试不与SQL Server建立连接。
对于单元测试,请添加以下文件:
DbContextMocker.cs文件的代码:
using Microsoft.EntityFrameworkCore;using WideWorldImporters.API.Models;namespace WideWorldImporters.API.UnitTests{ public static class DbContextMocker { public static WideWorldImportersDbContext GetWideWorldImportersDbContext(string dbName) { // Create options for DbContext instance var options = new DbContextOptionsBuilder() .UseInMemoryDatabase(databaseName: dbName) .Options; // Create instance of DbContext var dbContext = new WideWorldImportersDbContext(options); // Add entities in memory dbContext.Seed(); return dbContext; } }}
DbContextExtensions.cs文件的代码:
using System;using WideWorldImporters.API.Models;namespace WideWorldImporters.API.UnitTests{ public static class DbContextExtensions { public static void Seed(this WideWorldImportersDbContext dbContext) { // Add entities for DbContext instance dbContext.StockItems.Add(new StockItem { StockItemID = 1, StockItemName = "USB missile launcher (Green)", SupplierID = 12, UnitPackageID = 7, OuterPackageID = 7, LeadTimeDays = 14, QuantityPerOuter = 1, IsChillerStock = false, TaxRate = 15.000m, UnitPrice = 25.00m, RecommendedRetailPrice = 37.38m, TypicalWeightPerUnit = 0.300m, MarketingComments = "Complete with 12 projectiles", CustomFields = "{ \"CountryOfManufacture\": \"China\", \"Tags\": [\"USB Powered\"] }", Tags = "[\"USB Powered\"]", SearchDetails = "USB missile launcher (Green) Complete with 12 projectiles", LastEditedBy = 1, ValidFrom = Convert.ToDateTime("5/31/2016 11:11:00 PM"), ValidTo = Convert.ToDateTime("12/31/9999 11:59:59 PM") }); dbContext.StockItems.Add(new StockItem { StockItemID = 2, StockItemName = "USB rocket launcher (Gray)", SupplierID = 12, ColorID = 12, UnitPackageID = 7, OuterPackageID = 7, LeadTimeDays = 14, QuantityPerOuter = 1, IsChillerStock = false, TaxRate = 15.000m, UnitPrice = 25.00m, RecommendedRetailPrice = 37.38m, TypicalWeightPerUnit = 0.300m, MarketingComments = "Complete with 12 projectiles", CustomFields = "{ \"CountryOfManufacture\": \"China\", \"Tags\": [\"USB Powered\"] }", Tags = "[\"USB Powered\"]", SearchDetails = "USB rocket launcher (Gray) Complete with 12 projectiles", LastEditedBy = 1, ValidFrom = Convert.ToDateTime("5/31/2016 11:11:00 PM"), ValidTo = Convert.ToDateTime("12/31/9999 11:59:59 PM") }); dbContext.StockItems.Add(new StockItem { StockItemID = 3, StockItemName = "Office cube periscope (Black)", SupplierID = 12, ColorID = 3, UnitPackageID = 7, OuterPackageID = 6, LeadTimeDays = 14, QuantityPerOuter = 10, IsChillerStock = false, TaxRate = 15.000m, UnitPrice = 18.50m, RecommendedRetailPrice = 27.66m, TypicalWeightPerUnit = 0.250m, MarketingComments = "Need to see over your cubicle wall? This is just what's needed.", CustomFields = "{ \"CountryOfManufacture\": \"China\", \"Tags\": [] }", Tags = "[]", SearchDetails = "Office cube periscope (Black) Need to see over your cubicle wall? This is just what's needed.", LastEditedBy = 1, ValidFrom = Convert.ToDateTime("5/31/2016 11:00:00 PM"), ValidTo = Convert.ToDateTime("12/31/9999 11:59:59 PM") }); dbContext.StockItems.Add(new StockItem { StockItemID = 4, StockItemName = "USB food flash drive - sushi roll", SupplierID = 12, UnitPackageID = 7, OuterPackageID = 7, LeadTimeDays = 14, QuantityPerOuter = 1, IsChillerStock = false, TaxRate = 15.000m, UnitPrice = 32.00m, RecommendedRetailPrice = 47.84m, TypicalWeightPerUnit = 0.050m, CustomFields = "{ \"CountryOfManufacture\": \"Japan\", \"Tags\": [\"32GB\",\"USB Powered\"] }", Tags = "[\"32GB\",\"USB Powered\"]", SearchDetails = "USB food flash drive - sushi roll ", LastEditedBy = 1, ValidFrom = Convert.ToDateTime("5/31/2016 11:11:00 PM"), ValidTo = Convert.ToDateTime("12/31/9999 11:59:59 PM") }); dbContext.StockItems.Add(new StockItem { StockItemID = 5, StockItemName = "USB food flash drive - hamburger", SupplierID = 12, UnitPackageID = 7, OuterPackageID = 7, LeadTimeDays = 14, QuantityPerOuter = 1, IsChillerStock = false, TaxRate = 15.000m, UnitPrice = 32.00m, RecommendedRetailPrice = 47.84m, TypicalWeightPerUnit = 0.050m, CustomFields = "{ \"CountryOfManufacture\": \"Japan\", \"Tags\": [\"16GB\",\"USB Powered\"] }", Tags = "[\"16GB\",\"USB Powered\"]", SearchDetails = "USB food flash drive - hamburger ", LastEditedBy = 1, ValidFrom = Convert.ToDateTime("5/31/2016 11:11:00 PM"), ValidTo = Convert.ToDateTime("12/31/9999 11:59:59 PM") }); dbContext.StockItems.Add(new StockItem { StockItemID = 6, StockItemName = "USB food flash drive - hot dog", SupplierID = 12, UnitPackageID = 7, OuterPackageID = 7, LeadTimeDays = 14, QuantityPerOuter = 1, IsChillerStock = false, TaxRate = 15.000m, UnitPrice = 32.00m, RecommendedRetailPrice = 47.84m, TypicalWeightPerUnit = 0.050m, CustomFields = "{ \"CountryOfManufacture\": \"Japan\", \"Tags\": [\"32GB\",\"USB Powered\"] }", Tags = "[\"32GB\",\"USB Powered\"]", SearchDetails = "USB food flash drive - hot dog ", LastEditedBy = 1, ValidFrom = Convert.ToDateTime("5/31/2016 11:11:00 PM"), ValidTo = Convert.ToDateTime("12/31/9999 11:59:59 PM") }); dbContext.StockItems.Add(new StockItem { StockItemID = 7, StockItemName = "USB food flash drive - pizza slice", SupplierID = 12, UnitPackageID = 7, OuterPackageID = 7, LeadTimeDays = 14, QuantityPerOuter = 1, IsChillerStock = false, TaxRate = 15.000m, UnitPrice = 32.00m, RecommendedRetailPrice = 47.84m, TypicalWeightPerUnit = 0.050m, CustomFields = "{ \"CountryOfManufacture\": \"Japan\", \"Tags\": [\"16GB\",\"USB Powered\"] }", Tags = "[\"16GB\",\"USB Powered\"]", SearchDetails = "USB food flash drive - pizza slice ", LastEditedBy = 1, ValidFrom = Convert.ToDateTime("5/31/2016 11:11:00 PM"), ValidTo = Convert.ToDateTime("12/31/9999 11:59:59 PM") }); dbContext.StockItems.Add(new StockItem { StockItemID = 8, StockItemName = "USB food flash drive - dim sum 10 drive variety pack", SupplierID = 12, UnitPackageID = 9, OuterPackageID = 9, LeadTimeDays = 14, QuantityPerOuter = 1, IsChillerStock = false, TaxRate = 15.000m, UnitPrice = 240.00m, RecommendedRetailPrice = 358.80m, TypicalWeightPerUnit = 0.500m, CustomFields = "{ \"CountryOfManufacture\": \"Japan\", \"Tags\": [\"32GB\",\"USB Powered\"] }", Tags = "[\"32GB\",\"USB Powered\"]", SearchDetails = "USB food flash drive - dim sum 10 drive variety pack ", LastEditedBy = 1, ValidFrom = Convert.ToDateTime("5/31/2016 11:11:00 PM"), ValidTo = Convert.ToDateTime("12/31/9999 11:59:59 PM") }); dbContext.StockItems.Add(new StockItem { StockItemID = 9, StockItemName = "USB food flash drive - banana", SupplierID = 12, UnitPackageID = 7, OuterPackageID = 7, LeadTimeDays = 14, QuantityPerOuter = 1, IsChillerStock = false, TaxRate = 15.000m, UnitPrice = 32.00m, RecommendedRetailPrice = 47.84m, TypicalWeightPerUnit = 0.050m, CustomFields = "{ \"CountryOfManufacture\": \"Japan\", \"Tags\": [\"16GB\",\"USB Powered\"] }", Tags = "[\"16GB\",\"USB Powered\"]", SearchDetails = "USB food flash drive - banana ", LastEditedBy = 1, ValidFrom = Convert.ToDateTime("5/31/2016 11:11:00 PM"), ValidTo = Convert.ToDateTime("12/31/9999 11:59:59 PM") }); dbContext.StockItems.Add(new StockItem { StockItemID = 10, StockItemName = "USB food flash drive - chocolate bar", SupplierID = 12, UnitPackageID = 7, OuterPackageID = 7, LeadTimeDays = 14, QuantityPerOuter = 1, IsChillerStock = false, TaxRate = 15.000m, UnitPrice = 32.00m, RecommendedRetailPrice = 47.84m, TypicalWeightPerUnit = 0.050m, CustomFields = "{ \"CountryOfManufacture\": \"Japan\", \"Tags\": [\"32GB\",\"USB Powered\"] }", Tags = "[\"32GB\",\"USB Powered\"]", SearchDetails = "USB food flash drive - chocolate bar ", LastEditedBy = 1, ValidFrom = Convert.ToDateTime("5/31/2016 11:11:00 PM"), ValidTo = Convert.ToDateTime("12/31/9999 11:59:59 PM") }); dbContext.StockItems.Add(new StockItem { StockItemID = 11, StockItemName = "USB food flash drive - cookie", SupplierID = 12, UnitPackageID = 7, OuterPackageID = 7, LeadTimeDays = 14, QuantityPerOuter = 1, IsChillerStock = false, TaxRate = 15.000m, UnitPrice = 32.00m, RecommendedRetailPrice = 47.84m, TypicalWeightPerUnit = 0.050m, CustomFields = "{ \"CountryOfManufacture\": \"Japan\", \"Tags\": [\"16GB\",\"USB Powered\"] }", Tags = "[\"16GB\",\"USB Powered\"]", SearchDetails = "USB food flash drive - cookie ", LastEditedBy = 1, ValidFrom = Convert.ToDateTime("5/31/2016 11:11:00 PM"), ValidTo = Convert.ToDateTime("12/31/9999 11:59:59 PM") }); dbContext.StockItems.Add(new StockItem { StockItemID = 12, StockItemName = "USB food flash drive - donut", SupplierID = 12, UnitPackageID = 7, OuterPackageID = 7, LeadTimeDays = 14, QuantityPerOuter = 1, IsChillerStock = false, TaxRate = 15.000m, UnitPrice = 32.00m, RecommendedRetailPrice = 47.84m, TypicalWeightPerUnit = 0.050m, CustomFields = "{ \"CountryOfManufacture\": \"Japan\", \"Tags\": [\"32GB\",\"USB Powered\"] }", Tags = "[\"32GB\",\"USB Powered\"]", SearchDetails = "USB food flash drive - donut ", LastEditedBy = 1, ValidFrom = Convert.ToDateTime("5/31/2016 11:11:00 PM"), ValidTo = Convert.ToDateTime("12/31/9999 11:59:59 PM") }); dbContext.SaveChanges(); } }}
WarehouseControllerUnitTest.cs文件的代码:
using System;using System.Threading.Tasks;using Microsoft.AspNetCore.Mvc;using WideWorldImporters.API.Controllers;using WideWorldImporters.API.Models;using Xunit;namespace WideWorldImporters.API.UnitTests{ public class WarehouseControllerUnitTest { [Fact] public async Task TestGetStockItemsAsync() { // Arrange var dbContext = DbContextMocker.GetWideWorldImportersDbContext(nameof(TestGetStockItemsAsync)); var controller = new WarehouseController(null, dbContext); // Act var response = await controller.GetStockItemsAsync() as ObjectResult; var value = response.Value as IPagedResponse; dbContext.Dispose(); // Assert Assert.False(value.DidError); } [Fact] public async Task TestGetStockItemAsync() { // Arrange var dbContext = DbContextMocker.GetWideWorldImportersDbContext(nameof(TestGetStockItemAsync)); var controller = new WarehouseController(null, dbContext); var id = 1; // Act var response = await controller.GetStockItemAsync(id) as ObjectResult; var value = response.Value as ISingleResponse ; dbContext.Dispose(); // Assert Assert.False(value.DidError); } [Fact] public async Task TestPostStockItemAsync() { // Arrange var dbContext = DbContextMocker.GetWideWorldImportersDbContext(nameof(TestPostStockItemAsync)); var controller = new WarehouseController(null, dbContext); var requestModel = new PostStockItemsRequest { StockItemID = 100, StockItemName = "USB anime flash drive - Goku", SupplierID = 12, UnitPackageID = 7, OuterPackageID = 7, LeadTimeDays = 14, QuantityPerOuter = 1, IsChillerStock = false, TaxRate = 15.000m, UnitPrice = 32.00m, RecommendedRetailPrice = 47.84m, TypicalWeightPerUnit = 0.050m, CustomFields = "{ \"CountryOfManufacture\": \"Japan\", \"Tags\": [\"32GB\",\"USB Powered\"] }", Tags = "[\"32GB\",\"USB Powered\"]", SearchDetails = "USB anime flash drive - Goku", LastEditedBy = 1, ValidFrom = DateTime.Now, ValidTo = DateTime.Now.AddYears(5) }; // Act var response = await controller.PostStockItemAsync(requestModel) as ObjectResult; var value = response.Value as ISingleResponse ; dbContext.Dispose(); // Assert Assert.False(value.DidError); } [Fact] public async Task TestPutStockItemAsync() { // Arrange var dbContext = DbContextMocker.GetWideWorldImportersDbContext(nameof(TestPutStockItemAsync)); var controller = new WarehouseController(null, dbContext); var id = 12; var requestModel = new PutStockItemsRequest { StockItemName = "USB food flash drive (Update)", SupplierID = 12, ColorID = 3 }; // Act var response = await controller.PutStockItemAsync(id, requestModel) as ObjectResult; var value = response.Value as IResponse; dbContext.Dispose(); // Assert Assert.False(value.DidError); } [Fact] public async Task TestDeleteStockItemAsync() { // Arrange var dbContext = DbContextMocker.GetWideWorldImportersDbContext(nameof(TestDeleteStockItemAsync)); var controller = new WarehouseController(null, dbContext); var id = 5; // Act var response = await controller.DeleteStockItemAsync(id) as ObjectResult; var value = response.Value as IResponse; dbContext.Dispose(); // Assert Assert.False(value.DidError); } }}
我们可以看到,WarehouseControllerUnitTest包含Web API的所有测试,这些是方法:
方法 | 描述 |
TestGetStockItemsAsync | 检索库存商品 |
TestGetStockItemAsync | 按ID检索现有库存料品 |
TestPostStockItemAsync | 创建新的库存项目 |
TestPutStockItemAsync | 更新现有库存项目 |
TestDeleteStockItemAsync | 删除现有库存项目 |
单元测试如何工作?
DbContextMocker在内存数据库中创建一个WideWorldImportersDbContext实例,该dbName参数设置内存数据库中的名称; 然后有一个Seed方法的调用,这个方法添加WideWorldImportersDbContext实例的实体以提供结果。
DbContextExtensions类包含Seed扩展方法。
WarehouseControllerUnitTest类包含类的所有对WarehouseController类的测试。
请记住,每个测试在每个测试方法内部使用不同的内存数据库。我们使用nameof运算符的测试方法名称在内存数据库中检索。
在这个级别(单元测试),我们只需要检查存储库的操作,不需要使用SQL数据库(关系,事务等)。
单元测试的过程是:
运行单元测试
保存所有更改并构建WideWorldImporters.API.UnitTests项目。
现在,检查测试资源管理器中的测试:
使用测试资源管理器运行所有测试,如果出现任何错误,请检查错误消息,查看代码并重复此过程。
要为API项目添加集成测试,请按照下列步骤操作:
管理WideWorldImporters.API.IntegrationTests项目的引用:
现在添加WideWorldImporters.API项目的引用:
创建项目后,为项目添加以下NuGet包:
删除UnitTest1.cs文件。
保存更改并构建WideWorldImporters.API.IntegrationTests项目。
单元测试和集成测试有什么区别?对于单元测试,我们模拟Web API项目和集成测试的所有依赖项,我们运行一个模拟Web API执行的过程,这意味着Http请求。
现在我们继续添加与集成测试相关的代码。
对于这个项目,集成测试将执行Http请求,每个Http请求将对SQL Server实例中的现有数据库执行操作。我们将使用SQL Server的本地实例,这可以根据您的工作环境进行更改,我的意思是集成测试的范围。
TestFixture.cs文件的代码:
using System;using System.IO;using System.Net.Http;using System.Net.Http.Headers;using System.Reflection;using Microsoft.AspNetCore.Hosting;using Microsoft.AspNetCore.Mvc.ApplicationParts;using Microsoft.AspNetCore.Mvc.Controllers;using Microsoft.AspNetCore.Mvc.ViewComponents;using Microsoft.AspNetCore.TestHost;using Microsoft.Extensions.Configuration;using Microsoft.Extensions.DependencyInjection;namespace WideWorldImporters.API.IntegrationTests{ public class TestFixture: IDisposable { public static string GetProjectPath(string projectRelativePath, Assembly startupAssembly) { var projectName = startupAssembly.GetName().Name; var applicationBasePath = AppContext.BaseDirectory; var directoryInfo = new DirectoryInfo(applicationBasePath); do { directoryInfo = directoryInfo.Parent; var projectDirectoryInfo = new DirectoryInfo (Path.Combine(directoryInfo.FullName, projectRelativePath)); if (projectDirectoryInfo.Exists) if (new FileInfo(Path.Combine(projectDirectoryInfo.FullName, projectName, $"{projectName}.csproj")).Exists) return Path.Combine(projectDirectoryInfo.FullName, projectName); } while (directoryInfo.Parent != null); throw new Exception($"Project root could not be located using the application root {applicationBasePath}."); } private TestServer Server; public TestFixture() : this(Path.Combine("")) { } protected TestFixture(string relativeTargetProjectParentDir) { var startupAssembly = typeof(TStartup).GetTypeInfo().Assembly; var contentRoot = GetProjectPath(relativeTargetProjectParentDir, startupAssembly); var configurationBuilder = new ConfigurationBuilder() .SetBasePath(contentRoot) .AddJsonFile("appsettings.json"); var webHostBuilder = new WebHostBuilder() .UseContentRoot(contentRoot) .ConfigureServices(InitializeServices) .UseConfiguration(configurationBuilder.Build()) .UseEnvironment("Development") .UseStartup(typeof(TStartup)); Server = new TestServer(webHostBuilder); Client = Server.CreateClient(); Client.BaseAddress = new Uri("http://localhost:1234"); Client.DefaultRequestHeaders.Accept.Clear(); Client.DefaultRequestHeaders.Accept.Add (new MediaTypeWithQualityHeaderValue("application/json")); } public void Dispose() { Client.Dispose(); Server.Dispose(); } public HttpClient Client { get; } protected virtual void InitializeServices(IServiceCollection services) { var startupAssembly = typeof(TStartup).GetTypeInfo().Assembly; var manager = new ApplicationPartManager(); manager.ApplicationParts.Add(new AssemblyPart(startupAssembly)); manager.FeatureProviders.Add(new ControllerFeatureProvider()); manager.FeatureProviders.Add(new ViewComponentFeatureProvider()); services.AddSingleton(manager); } }}
ContentHelper.cs文件的代码:
using System.Net.Http;using System.Text;using Newtonsoft.Json;namespace WideWorldImporters.API.IntegrationTests{ public static class ContentHelper { public static StringContent GetStringContent(object obj) => new StringContent(JsonConvert.SerializeObject(obj), Encoding.Default, "application/json"); }}
WarehouseTests.cs文件的代码:
using System;using System.Net.Http;using System.Threading.Tasks;using Newtonsoft.Json;using WideWorldImporters.API.Models;using Xunit;namespace WideWorldImporters.API.IntegrationTests{ public class WarehouseTests : IClassFixture> { private HttpClient Client; public WarehouseTests(TestFixture fixture) { Client = fixture.Client; } [Fact] public async Task TestGetStockItemsAsync() { // Arrange var request = "/api/v1/Warehouse/StockItem"; // Act var response = await Client.GetAsync(request); // Assert response.EnsureSuccessStatusCode(); } [Fact] public async Task TestGetStockItemAsync() { // Arrange var request = "/api/v1/Warehouse/StockItem/1"; // Act var response = await Client.GetAsync(request); // Assert response.EnsureSuccessStatusCode(); } [Fact] public async Task TestPostStockItemAsync() { // Arrange var request = "/api/v1/Warehouse/StockItem"; var requestModel = new { StockItemName = string.Format("USB anime flash drive - Vegeta {0}", Guid.NewGuid()), SupplierID = 12, UnitPackageID = 7, OuterPackageID = 7, LeadTimeDays = 14, QuantityPerOuter = 1, IsChillerStock = false, TaxRate = 15.000m, UnitPrice = 32.00m, RecommendedRetailPrice = 47.84m, TypicalWeightPerUnit = 0.050m, CustomFields = "{ \"CountryOfManufacture\": \"Japan\", \"Tags\": [\"32GB\",\"USB Powered\"] }", Tags = "[\"32GB\",\"USB Powered\"]", SearchDetails = "USB anime flash drive - Vegeta", LastEditedBy = 1, ValidFrom = DateTime.Now, ValidTo = DateTime.Now.AddYears(5) }; // Act var response = await Client.PostAsync (request, ContentHelper.GetStringContent(request)); // Assert response.EnsureSuccessStatusCode(); } [Fact] public async Task TestPutStockItemAsync() { // Arrange var requestUrl = "/api/v1/Warehouse/StockItem/1"; var requestModel = new { StockItemName = string.Format("USB anime flash drive - Vegeta {0}", Guid.NewGuid()), SupplierID = 12, Color = 3, UnitPrice = 39.00m }; // Act var response = await Client.PutAsync (requestUrl, ContentHelper.GetStringContent(requestModel)); // Assert response.EnsureSuccessStatusCode(); } [Fact] public async Task TestDeleteStockItemAsync() { // Arrange var postRequest = "/api/v1/Warehouse/StockItem"; var requestModel = new { StockItemName = string.Format("Product to delete {0}", Guid.NewGuid()), SupplierID = 12, UnitPackageID = 7, OuterPackageID = 7, LeadTimeDays = 14, QuantityPerOuter = 1, IsChillerStock = false, TaxRate = 10.000m, UnitPrice = 10.00m, RecommendedRetailPrice = 47.84m, TypicalWeightPerUnit = 0.050m, CustomFields = "{ \"CountryOfManufacture\": \"USA\", \"Tags\": [\"Sample\"] }", Tags = "[\"Sample\"]", SearchDetails = "Product to delete", LastEditedBy = 1, ValidFrom = DateTime.Now, ValidTo = DateTime.Now.AddYears(5) }; // Act var postResponse = await Client.PostAsync (postRequest, ContentHelper.GetStringContent(requestModel)); var jsonFromPostResponse = await postResponse.Content.ReadAsStringAsync(); var singleResponse = JsonConvert.DeserializeObject >(jsonFromPostResponse); var deleteResponse = await Client.DeleteAsync (string.Format("/api/v1/Warehouse/StockItem/{0}", singleResponse.Model.StockItemID)); // Assert postResponse.EnsureSuccessStatusCode(); Assert.False(singleResponse.DidError); deleteResponse.EnsureSuccessStatusCode(); } }}
我们可以看到,WarehouseTests包含Web API的所有测试,这些是方法:
方法 | 描述 |
TestGetStockItemsAsync | 检索库存商品 |
TestGetStockItemAsync | 按ID检索现有库存料品 |
TestPostStockItemAsync | 创建新的库存项目 |
TestPutStockItemAsync | 更新现有库存项目 |
TestDeleteStockItemAsync | 删除现有库存项目 |
集成测试如何工作?
TestFixture<TStartup>类为Web API提供了一个Http客户端,使用项目中Startup的类作为为客户端应用配置的引用。
WarehouseTests类包含发送Web API的Http请求的所有方法,Http客户端的端口号是1234。
ContentHelper类包含一个帮助方法,可以从请求模型创建StringContent为JSON,这适用于POST和PUT请求。
集成测试的过程是:
运行集成测试
保存所有更改并构建WideWorldImporters.API.IntegrationTests项目,测试资源管理器将显示项目中的所有测试:
请记住:要执行集成测试,您需要运行SQL Server实例,appsettings.json文件中的连接字符串将用于与SQL Server建立连接。
现在运行所有集成测试,测试资源管理器如下图所示:
如果执行集成测试时出现任何错误,请检查错误消息,查看代码并重复此过程。
此时,您具备扩展API的技能,将此作为挑战并添加以下测试(单元测试和集成测试):
测试 | 描述 |
按参数获取库存商品 | 为使库存物品通过搜索请求lastEditedBy,colorID,outerPackageID,supplierID,unitPackageID的参数。 |
获取不存在的库存商品 | 使用不存在的ID获取库存项并检查Web API返回NotFound(404)状态。 |
添加具有现有名称的库存项目 | 添加具有现有名称的库存项,并检查Web API返回BadRequest(400)状态。 |
添加没有必填字段的库存商品 | 添加没有必填字段的库存项目并检查Web API返回BadRequest(400)状态。 |
更新不存在的库存项目 | 使用不存在的ID更新库存项目并检查Web API返回NotFound(404)状态。 |
更新现有库存项目而不包含必填字段 | 更新没有必填字段的现有库存项,并检查Web API返回BadRequest(400)状态。 |
删除不存在的库存项目 | 使用不存在的ID删除库存项目并检查Web API返回NotFound(404)状态。 |
删除包含订单的库存商品 | 使用不存在的ID删除库存项目并检查Web API返回NotFound(404)状态。 |
遵循单元和集成测试中使用的约定来完成此挑战。
祝好运!
原文地址:
转载地址:http://tzzhj.baihongyu.com/