本文是ABP官方文档翻译版,翻译基于 3.2.5 版本
官方文档分四部分
一、 教程文档
二、ABP 框架
三、zero 模块
四、其他(中文翻译资源)
本篇是第一部分的第一篇。
第一部分分三篇
1-1 手把手引进门
1-2 进阶
1-3 杂项 (相关理论知识)
第一篇含两个步骤。
1-1-1 ASP.NET Core & Entity Framework Core 后端(内核)含两篇 (第二篇链接)
1-1-2 ASP.NET MVC, Web API, EntityFramework & AngularJs 前端
现在进入正文
使用 ASP.NET Core, Entity Framework Core 和 ASP.NET Boilerplate 创建N层Web应用
土牛语录:
以下是手把手引进门教程,基于 ASP.NET Core, Entity Framework Core ,ABP 框架 创建Web 应用, PS: 自带自动的测试模块哦。
本文目录如下:
介绍
前期准备
创建应用
正式开发
创建任务实体 Entity
将任务添加到数据库上下文 DbContext
创建第一个数据迁移
创建数据库
编写任务服务
测试任务服务
任务列表展示
添加菜单
创建任务 Controller 和 视图模型
任务列表页面
本地化
任务过滤
任务列表页面的自动化测试
其他相关内容
文章修改历史
版权所有
介绍
这是系列文章的第一部分:使用 ASP.NET Core, Entity Framework Core 和 ASP.NET Boilerplate 创建N层Web应用
在本文中,我将指导大家创建一个样例(跨平台的多层Web应用),该样例会用到如下工具(请读者提前准备):
- Net Core 跨平台应用的基础开发框架
- ASP.NET Boilerplate (ABP) 开发的基础框架模板
- ASP.NET Core web开发框架
- Entity Framework Core ORM 数据框架
- Twitter Bootstrap HTML&CSS 前端开发框架
- jQuery 客户端 AJAX/DOM 类库
- xUnit 和 Shouldly 服务端测试工具(单元测试/集成测试)
ABP 框架中会默认使用 Log4Net 和 AutoMapper 。
我们同时还会使用以下技术:
- Layered Architecture 分层架构
- Domain Driven Design (DDD) DDD领域模型
- Dependency injection (DI) DI 依赖注入
- Integration Testing 集成测试
演示的开发项目是一个简单的任务管理应用,用于将任务分配出去。我不会一层一层的进行开发,而是随着应用的拓展直接切换所需的层次。随着应用的推拓展,我将会介绍所需的ABP和其他框架的特性。
前期准备
开发样例时需要以下工具,请提前在你的机器上进行安装:
- Visual Studio 2017
- SQL Server (你可以更改连接字符串为 localdb)
- Visual Studio Extensions:
创建应用
首先使用ABP模版(http://www.aspnetboilerplate.com/Templates)创建一个web应用项目,命名为"Acme.SimpleTaskApp" 。创建模板时可以设置自己的公司名称(比如Acme)。
本样例使用MPA(Multi Page Web Application)多页面模式(注:即使用MVC和Razor技术)进行开发,本文不使用SPA(注:土牛的SPA是使用Angular)单页面模式。同时为了使用最基础的开发模板功能,本文不使用Module Zero模块。
ABP 模版会创建一个多层的解决方案,如下图:
模板会根据输入的名字自动创建6个项目。
- core 领域层/业务层,包含实体Entity,领域服务 domain service 等等
- Application 应用层 , 包含DTO,应用服务 application service 等等
- Entity Framework 基础设施层 ,EF core 数据库集成处理 (从其他层抽象出来的EF core)
- Web 展示层 , 即Asp.net MVC层
- Tests 单元测试和集成测试,含应用层,领域层,基础设施层,不含Web展示层
- Web.Tests ASP.NET Core集成测试,包含web展示层的全部集成测试
以上是没有选择zero的项目结果,如果你选择了zero,项目结构就会变成下图:
当你把应用运行起来后,你会看到下图所示的用户界面:
这个应用包含一个顶级菜单栏,包含空的首页,关于页,还有一个语言的下拉选项。
正式开发
创建任务实体 Entity
我们从创建一个简单的任务实体 Task Entity 开始,由于它属于领域层,把它加到 core 项目里。
代码如下:
1 using System; 2 using System.ComponentModel.DataAnnotations; 3 using System.ComponentModel.DataAnnotations.Schema; 4 using Abp.Domain.Entities; 5 using Abp.Domain.Entities.Auditing; 6 using Abp.Timing; 7 8 namespace Acme.SimpleTaskApp.Tasks 9 { 10 [Table("AppTasks")] 11 public class Task : Entity, IHasCreationTime 12 { 13 public const int MaxTitleLength = 256; 14 public const int MaxDescriptionLength = 64 * 1024; //64KB 15 16 [Required] 17 [MaxLength(MaxTitleLength)] 18 public string Title { get; set; } 19 20 [MaxLength(MaxDescriptionLength)] 21 public string Description { get; set; } 22 23 public DateTime CreationTime { get; set; } 24 25 public TaskState State { get; set; } 26 27 public Task() 28 { 29 CreationTime = Clock.Now; 30 State = TaskState.Open; 31 } 32 33 public Task(string title, string description = null) 34 : this() 35 { 36 Title = title; 37 Description = description; 38 } 39 } 40 41 public enum TaskState : byte 42 { 43 Open = 0, 44 Completed = 1 45 } 46 }
- Task 实体从 ABP 的 Entity 基类继承,Entity 基类默认ID属性是 int 类型。如果主键类型为非 int 类型,也可以选择范型版本的 Entity<TPrimaryKey>.
- IHasCreationTime 是一个简单的接口,只定义了 CreationTime 属性 (统一规范 CreationTime 的名字)
- Task 实体定义了一个必填的 Title 和 非必填的 Description
- TaskState 是一个简单枚举,定义了 Task 任务的状态
- Clock.Now 返回默认的 DateTime.Now 。但它提供了一个抽象方法,使得我们可以在将来有需要的时候很轻松就可以转换为 DateTime.UtcNow 。在 ABP 框架中总是使用Clock.Now 而不使用 DateTime.Now 。
- 将 Task 实体存储到数据库的 AppTasks 表中。
将任务添加到数据库上下文 DbContext
.EntityFrameworkCore 包含一个预定义的 DbContext 。将 Task 实体的 DbSet 加到 DbContext 里。
代码如下:
1 public class SimpleTaskAppDbContext : AbpDbContext 2 { 3 public DbSet<Task> Tasks { get; set; } 4 5 public SimpleTaskAppDbContext(DbContextOptions<SimpleTaskAppDbContext> options) 6 : base(options) 7 { 8 9 } 10 }
现在,EF core 知道我们有了一个 Task 的实体。
创建第一个数据迁移
我们将创建一个初始化数据库迁移文件,它会自动创建数据库和数据库表 AppTasks 。打开源管理器 Package Manager Console from Visual Studio , 执行 Add-Migration 命令(默认的项目必须是 .EntityFrameworkCore 项目),如图:
这个命令会在 . EntityFrameworkCore 项目下创建一个迁移( Migrations )文件夹,文件夹包含一个迁移类和数据库模型的快照,如图:
如下代码所示,自动创建了 “初始化 ( Initial )”迁移类:
1 public partial class Initial : Migration 2 { 3 protected override void Up(MigrationBuilder migrationBuilder) 4 { 5 migrationBuilder.CreateTable( 6 name: "AppTasks", 7 columns: table => new 8 { 9 Id = table.Column<int>(nullable: false) 10 .Annotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn), 11 CreationTime = table.Column<DateTime>(nullable: false), 12 Description = table.Column<string>(maxLength: 65536, nullable: true), 13 State = table.Column<byte>(nullable: false), 14 Title = table.Column<string>(maxLength: 256, nullable: false) 15 }, 16 constraints: table => 17 { 18 table.PrimaryKey("PK_AppTasks", x => x.Id); 19 }); 20 } 21 22 protected override void Down(MigrationBuilder migrationBuilder) 23 { 24 migrationBuilder.DropTable( 25 name: "AppTasks"); 26 } 27 }
当我们执行数据库迁移命令时,这些代码会创建 AppTasks 表 (更多迁移相关信息请参照 entity framework documentation )
创建数据库
以上的迁移执行完毕后(注:Add-Migration 命令执行后),在包管理控制台中执行 Update-Database 命令,如下图:
这个命令将在 local SQL Server 中创建一个名为 “SimpleTaskAppDb” 的数据库并执行数据库迁移(此时,我们只有一个“初始化 ( Initial )”的迁移):
现在,我们有了 Task 实体,并且在数据库中有对应的数据库表, 我们输入一些简单的任务到表里。
友情提示: 数据库上下文字符串 connection string 在 .web 应用的 appsettings.json 中。 (要换数据库的自己改一下字符串哦)。
编写任务服务
Application Services 应用层服务用于将领域业务逻辑暴露给展示层。展示层在必要时通过使用 Data Transfer Object 数据传输对象(DTO)作用参数调用一个应用服务,应用服务则通过调用领域对象执行一些具体的业务逻辑并在有需要时返回一个DTO给展示层。
我们在 .Application 项目中创建第一个应用服务 TaskAppService ,该服务将执行与任务相关的应用程序逻辑。首先,我们先来定义一个app 服务接口:
代码如下:
1 public interface ITaskAppService : IApplicationService 2 { 3 Task<ListResultDto<TaskListDto>> GetAll(GetAllTasksInput input); 4 }
我们推荐先定义接口,但不是非这样做不可。按照惯例,ABP 中所有的应用服务都需要实现 IApplicationService 接口 (它只是一个空的标记接口)。我们创建了一个 GetAll 方法去查询任务列表,同时,我们定义了如下的 DTOs :
代码如下:
1 public class GetAllTasksInput 2 { 3 public TaskState? State { get; set; } 4 } 5 6 [AutoMapFrom(typeof(Task))] 7 public class TaskListDto : EntityDto, IHasCreationTime 8 { 9 public string Title { get; set; } 10 11 public string Description { get; set; } 12 13 public DateTime CreationTime { get; set; } 14 15 public TaskState State { get; set; } 16 }
- GetAllTasksInput DTO 为 GetAll 应用服务方法定义了一个输入参数 。 我们将 状态 state 定义为 DTO 对象 而不定义为方法的参数。 这样我们将来需要的时候可以在这个DTO增加其他的参数,同时兼容现有的客户端 (当然我们也可以在方法里加一个 state 参数)。
- TaskListDto 用开返回任务数据。该Dto 从 EntityDto 继承,EntityDto 只是定义了 Id 属性(我们可以不继承 EntityDto ,直接自己将 Id 加到我们的Dto里)。我们定义了[AutoMapFrom] 特性来创建 AutoMapper 自动映射任务实体到任务列表Dto TaskListDto 。这个特性在 Abp.AutoMapper nuget 包里进行了定义。
- ListResultDto 是一个简单的类,包含了一个列表(我们可以直接返回一个 List<TaskListDto> 列表)。
现在,我们可以实现 ITaskAppService 了。
代码如下:
1 using System.Collections.Generic; 2 using System.Linq; 3 using System.Threading.Tasks; 4 using Abp.Application.Services.Dto; 5 using Abp.Domain.Repositories; 6 using Abp.Linq.Extensions; 7 using Acme.SimpleTaskApp.Tasks.Dtos; 8 using Microsoft.EntityFrameworkCore; 9 10 namespace Acme.SimpleTaskApp.Tasks 11 { 12 public class TaskAppService : SimpleTaskAppAppServiceBase, ITaskAppService 13 { 14 private readonly IRepository<Task> _taskRepository; 15 16 public TaskAppService(IRepository<Task> taskRepository) 17 { 18 _taskRepository = taskRepository; 19 } 20 21 public async Task<ListResultDto<TaskListDto>> GetAll(GetAllTasksInput input) 22 { 23 var tasks = await _taskRepository 24 .GetAll() 25 .WhereIf(input.State.HasValue, t => t.State == input.State.Value) 26 .OrderByDescending(t => t.CreationTime) 27 .ToListAsync(); 28 29 return new ListResultDto<TaskListDto>( 30 ObjectMapper.Map<List<TaskListDto>>(tasks) 31 ); 32 } 33 } 34 }
- TaskAppService 该类从 SimpleTaskAppAppServiceBase 继承,SimpleTaskAppAppServiceBase (从 ABP 的 ApplicationService 类继承)在模板里已经自动生成。 TaskAppService 不是必须从 SimpleTaskAppAppServiceBase 继承,应用服务可以是普通类。但是 ApplicationService 基类有一些预先注入的服务(就像这里使用的 ObjectMapper )
- 我们使用依赖注入 dependency injection 来获取数据仓储 repository
- Repositories 数据仓储用于为数据实体抽象数据库操作。ABP 为每个实体创建了预定义的数据库仓储(就像这里用到了 IRepository<Task> )用于实现通用的任务。IRepository.GetAll() 方法用于查询数据实体,它返回了一个 IQueryable 接口。
- WhereIf 这是 ABP 里的一个拓展方法,该方法提供了一个 IQueryable.Where 方法的简便条件语法。
- ObjectMapper 用于将任务对象列表映射到任务列表Dto对象列表 (基于 Application Service 基类并默认实现 AutoMapper )
测试任务服务
在创建用户接口钱,我们需要测试一下任务应用服务 TaskAppService 。 如果你对自动化测试不感兴趣的话,可以忽略这个部分。
我们的模板包含 .Tests 项目,这可以测试我们的代码。这个项目不使用 SQL Server数据库,而是使用EF core 的内存数据库。所以,我们可以不用真实数据库来进行单元测试。它为每个测试创建了单独的数据库。所以每个测试都是隔离的。我们需要在开始测试前使用 TestDataBuilder 类添加初始测试数据到内存数据库里。我修改了 TestDataBuilder 。
代码如下:
1 public class TestDataBuilder 2 { 3 private readonly SimpleTaskAppDbContext _context; 4 5 public TestDataBuilder(SimpleTaskAppDbContext context) 6 { 7 _context = context; 8 } 9 10 public void Build() 11 { 12 _context.Tasks.AddRange( 13 new Task("Follow the white rabbit", "Follow the white rabbit in order to know the reality."), 14 new Task("Clean your room") { State = TaskState.Completed } 15 ); 16 } 17 }
通过样例项目的源代码,你可以看懂 TestDataBuilder 在哪里用,具体怎么用。我们添加2个任务(其中一个已经完成)到数据库上下文 dbcontext 。我们可以假定数据库中有2个任务,开始编写测试用例。 第一个继承测试用来测试 TaskAppService.GetAll 方法。
代码如下:
1 public class TaskAppService_Tests : SimpleTaskAppTestBase 2 { 3 private readonly ITaskAppService _taskAppService; 4 5 public TaskAppService_Tests() 6 { 7 _taskAppService = Resolve<ITaskAppService>(); 8 } 9 10 [Fact] 11 public async System.Threading.Tasks.Task Should_Get_All_Tasks() 12 { 13 //Act 14 var output = await _taskAppService.GetAll(new GetAllTasksInput()); 15 16 //Assert 17 output.Items.Count.ShouldBe(2); 18 } 19 20 [Fact] 21 public async System.Threading.Tasks.Task Should_Get_Filtered_Tasks() 22 { 23 //Act 24 var output = await _taskAppService.GetAll(new GetAllTasksInput { State = TaskState.Open }); 25 26 //Assert 27 output.Items.ShouldAllBe(t => t.State == TaskState.Open); 28 } 29 }
我们创建2个不同的测试用例来测试 GetAll 方法。现在,我们打开测试浏览器(在VS主菜单的 TestWindowsTest Explorer 菜单下)开始进行单元测试。
所有测试均成功。最后一个我们现在可以忽略它,他是一个模板生成的测试。
友情提示: ABP 模板默认安装使用 xUnit 和 Shouldly 。我们使用它们编写我们的测试。
任务列表展示
现在,我们确定 TaskAppService 服务可以正常工作。 我们可以开始创建页面来展示所有的任务。
添加菜单
首先,我们在顶级菜单上添加一个新的菜单
代码如下
1 public class SimpleTaskAppNavigationProvider : NavigationProvider 2 { 3 public override void SetNavigation(INavigationProviderContext context) 4 { 5 context.Manager.MainMenu 6 .AddItem( 7 new MenuItemDefinition( 8 "Home", 9 L("HomePage"), 10 url: "", 11 icon: "fa fa-home" 12 ) 13 ).AddItem( 14 new MenuItemDefinition( 15 "About", 16 L("About"), 17 url: "Home/About", 18 icon: "fa fa-info" 19 ) 20 ).AddItem( 21 new MenuItemDefinition( 22 "TaskList", 23 L("TaskList"), 24 url: "Tasks", 25 icon: "fa fa-tasks" 26 ) 27 ); 28 } 29 30 private static ILocalizableString L(string name) 31 { 32 return new LocalizableString(name, SimpleTaskAppConsts.LocalizationSourceName); 33 } 34 }
模板自带两个页面:首页和关于页,如上代码所示。我们也可以修改它们创建新的页面。但现在我们不修改首页和关于页,我们创建新的菜单项。
创建任务 Controller 和 视图模型
我们在 .Web 项目下创建一个新的 controller 类,命名为 TasksController 。
代码如下
1 public class TasksController : SimpleTaskAppControllerBase 2 { 3 private readonly ITaskAppService _taskAppService; 4 5 public TasksController(ITaskAppService taskAppService) 6 { 7 _taskAppService = taskAppService; 8 } 9 10 public async Task<ActionResult> Index(GetAllTasksInput input) 11 { 12 var output = await _taskAppService.GetAll(input); 13 var model = new IndexViewModel(output.Items); 14 return View(model); 15 } 16 }
- TasksController 从 SimpleTaskAppControllerBase ( SimpleTaskAppControllerBase 从 AbpController 继承)继承,该类包含应用程序 Controllers 需要的通用基础代码。
- 我们反射了 ITaskAppService , 以获取到所有的任务列表。
- 我们在 .Web 项目中创建了一个 IndexViewModel 类来将数据展示到视图上,这样可以不直接将 GetAll 方法的结果直接暴露到视图上。
代码如下
1 public class IndexViewModel 2 { 3 public IReadOnlyList<TaskListDto> Tasks { get; } 4 5 public IndexViewModel(IReadOnlyList<TaskListDto> tasks) 6 { 7 Tasks = tasks; 8 } 9 10 public string GetTaskLabel(TaskListDto task) 11 { 12 switch (task.State) 13 { 14 case TaskState.Open: 15 return "label-success"; 16 default: 17 return "label-default"; 18 } 19 } 20 }
我们创建了一个简单的视图模型,在它的构造函数中,我们获取了一个任务列表(由 ITaskAppService 提供)。同时它还有一个 GetTaskLabel 方法,用于在视图中通过一个 选择 Bootstrap 标签来标示任务。
任务列表页面
最后,完成实际的 Index 视图。
代码如下
1 @model Acme.SimpleTaskApp.Web.Models.Tasks.IndexViewModel 2 3 @{ 4 ViewBag.Title = L("TaskList"); 5 ViewBag.ActiveMenu = "TaskList"; //Matches with the menu name in SimpleTaskAppNavigationProvider to highlight the menu item 6 } 7 8 <h2>@L("TaskList")</h2> 9 10 <div class="row"> 11 <div> 12 <ul class="list-group"> 13 @foreach (var task in Model.Tasks) 14 { 15 <li class="list-group-item"> 16 <span class="pull-right label @Model.GetTaskLabel(task)">@L($"TaskState_{task.State}")</span> 17 <h4 class="list-group-item-heading">@task.Title</h4> 18 <div class="list-group-item-text"> 19 @task.CreationTime.ToString("yyyy-MM-dd HH:mm:ss") 20 </div> 21 </li> 22 } 23 </ul> 24 </div> 25 </div>
我们使用 Bootstrap 的 list group 组件和定义好的模型来渲染视图。我们使用 IndexViewModel.GetTaskLable() 方法来获得任务的标签类型。渲染后的界面如下图:
本地化
我们在视图里使用 ABP 框架自带的 L 方法。 它用于本地化语言。我们在 .Core 项目下的 Localization/Source 文件夹中定义好了本地化字符串,使用 .json 文件。英语版本的本地化语言设置
代码如下
1 { 2 "culture": "en", 3 "texts": { 4 "HelloWorld": "Hello World!", 5 "ChangeLanguage": "Change language", 6 "HomePage": "HomePage", 7 "About": "About", 8 "Home_Description": "Welcome to SimpleTaskApp...", 9 "About_Description": "This is a simple startup template to use ASP.NET Core with ABP framework.", 10 "TaskList": "Task List", 11 "TaskState_Open": "Open", 12 "TaskState_Completed": "Completed" 13 } 14 }
模板自带了大多数的文本,当然,它们可以删除掉。在上面的代码中我只是加了最后的三行。使用 ABP 的本地化是相当的简单,如果你想了解本地化系统更多的信息,请查阅文档 localization document
任务过滤
正如之前说过的,TaskController 实际上使用的是 GetAllTasksInput ,可以灵活的过滤任务。我们可以添加一个任务列表的下拉菜单来过滤任务。首先,我们添加一个下拉菜单到视图上(我们加到 header 里):
代码如下
1 <h2> 2 @L("TaskList") 3 <span class="pull-right"> 4 @Html.DropDownListFor( 5 model => model.SelectedTaskState, 6 Model.GetTasksStateSelectListItems(LocalizationManager), 7 new 8 { 9 @class = "form-control", 10 id = "TaskStateCombobox" 11 }) 12 </span> 13 </h2>
然后我修改了 IndexViewModel , 增加了 SeletedTaskState 属性和 GetTaskStateSelectListItems 方法:
代码如下
1 public class IndexViewModel 2 { 3 //... 4 5 public TaskState? SelectedTaskState { get; set; } 6 7 public List<SelectListItem> GetTasksStateSelectListItems(ILocalizationManager localizationManager) 8 { 9 var list = new List<SelectListItem> 10 { 11 new SelectListItem 12 { 13 Text = localizationManager.GetString(SimpleTaskAppConsts.LocalizationSourceName, "AllTasks"), 14 Value = "", 15 Selected = SelectedTaskState == null 16 } 17 }; 18 19 list.AddRange(Enum.GetValues(typeof(TaskState)) 20 .Cast<TaskState>() 21 .Select(state => 22 new SelectListItem 23 { 24 Text = localizationManager.GetString(SimpleTaskAppConsts.LocalizationSourceName, $"TaskState_{state}"), 25 Value = state.ToString(), 26 Selected = state == SelectedTaskState 27 }) 28 ); 29 30 return list; 31 } 32 }
我们也可以在 controller 里设置 SelectedTaskState :
代码如下
1 public async Task<ActionResult> Index(GetAllTasksInput input) 2 { 3 var output = await _taskAppService.GetAll(input); 4 var model = new IndexViewModel(output.Items) 5 { 6 SelectedTaskState = input.State 7 }; 8 return View(model); 9 }
现在,我们运行程序,可以看到视图的右上角有个下拉框,如图:
我们添加了下拉框,但它现在还不能用。我们需要编写一些简单的 javascript 代码,当下拉框内容更改后可以重新请求/刷新任务列表页面。我们在 .Web 项目里创建了 wwwrootjsviews asksindex.js 文件
代码如下
1 (function ($) { 2 $(function () { 3 4 var _$taskStateCombobox = $('#TaskStateCombobox'); 5 6 _$taskStateCombobox.change(function() { 7 location.href = '/Tasks?state=' + _$taskStateCombobox.val(); 8 }); 9 10 }); 11 })(jQuery);
我们首先添加 Bundler & Minifier 扩展程序(这是 ASP.NET Core 项目标配的压缩文件)来压缩脚本的大小, 然后开始在视图里编写 javascript :
这将在 .Web 项目中的 bundleconfig.json 中添加以下代码
代码如下
1 { 2 "outputFileName": "wwwroot/js/views/tasks/index.min.js", 3 "inputFiles": [ 4 "wwwroot/js/views/tasks/index.js" 5 ] 6 }
同时创建了 script 的压缩版本
无论我何时修改了index.js , index.min.js 都会自动重新生成。现在,我们可以在我们的页面里插入 javascript 文件了。
代码如下
1 @section scripts 2 { 3 <environment names="Development"> 4 <script src="~/js/views/tasks/index.js"></script> 5 </environment> 6 7 <environment names="Staging,Production"> 8 <script src="~/js/views/tasks/index.min.js"></script> 9 </environment> 10 }
至此,我们的视图将在开发环境下使用 index.js 包,而在生产环境中使用 index.min.js (压缩版本)包。这是在 ASP.Net Core MVC 项目中通用的做法。
任务列表页面的自动化测试
ASP.NET Core MVC 基础框架中集成了一个继承测试模块。我们可以完整的测试我们的服务端代码了。如果你对自动化测试不感兴趣的话,你可以忽略这个部分。
ABP 模板中自带 .Web.Tests 项目。我们创建一个普通的测试来请求 TaskController.Index , 然后检查反馈内容:
代码如下
1 public class TasksController_Tests : SimpleTaskAppWebTestBase 2 { 3 [Fact] 4 public async System.Threading.Tasks.Task Should_Get_Tasks_By_State() 5 { 6 //Act 7 8 var response = await GetResponseAsStringAsync( 9 GetUrl<TasksController>(nameof(TasksController.Index), new 10 { 11 state = TaskState.Open 12 } 13 ) 14 ); 15 16 //Assert 17 18 response.ShouldNotBeNullOrWhiteSpace(); 19 } 20 }
GetResponseAsStringAsync 和 GetUrl 是 ABP 的 AbpAspNetCoreIntrgratedTestBase 类中很有用的方法。使用这些快捷方法我们可以比较容易的创建请求,如果直接使用客户端请求(一个 HttpClient 的实例)会相对复杂一些。如果想深入了解,请参考 ASP.NET Core 的 integration testing documentation
当我们开始 debug 测试模块式,我们可以看到反馈的 HTML 如下图
上图显示 Index 页面的反馈很正常。但是,我们更想知道返回的 HTML 是否正如我们所预期的那样。 有很多类库可以用来解析 HTML 。ABP 模板的 .Web.Tests 项目预先安装了其中的一个类库 AngleSharp 我们用它来检查创建的 HTML 代码。
代码如下
1 public class TasksController_Tests : SimpleTaskAppWebTestBase 2 { 3 [Fact] 4 public async System.Threading.Tasks.Task Should_Get_Tasks_By_State() 5 { 6 //Act 7 8 var response = await GetResponseAsStringAsync( 9 GetUrl<TasksController>(nameof(TasksController.Index), new 10 { 11 state = TaskState.Open 12 } 13 ) 14 ); 15 16 //Assert 17 18 response.ShouldNotBeNullOrWhiteSpace(); 19 20 //Get tasks from database 21 var tasksInDatabase = await UsingDbContextAsync(async dbContext => 22 { 23 return await dbContext.Tasks 24 .Where(t => t.State == TaskState.Open) 25 .ToListAsync(); 26 }); 27 28 //Parse HTML response to check if tasks in the database are returned 29 var document = new HtmlParser().Parse(response); 30 var listItems = document.QuerySelectorAll("#TaskList li"); 31 32 //Check task count 33 listItems.Length.ShouldBe(tasksInDatabase.Count); 34 35 //Check if returned list items are same those in the database 36 foreach (var listItem in listItems) 37 { 38 var header = listItem.QuerySelector(".list-group-item-heading"); 39 var taskTitle = header.InnerHtml.Trim(); 40 tasksInDatabase.Any(t => t.Title == taskTitle).ShouldBeTrue(); 41 } 42 } 43 }
你可以深入检查 HTML 的更多细节。但一般来说,检查基本的标签就够了。
其他相关内容
第二篇 Second article 接着开发这个应用服务。
(第二篇翻译链接)
文章修改历史
- 2017-07-30: 将文章中的 ListResultOutput 替换为 ListResultDto
- 2017-06-02: 将文章和解决方案修改为支持 .net core
- 2016-08-08: 添加 第二篇 的链接
- 2016-08-01: 第一次发布
版权所有
该文章和其中的任何源代码和文件的版权均归 The Code Project Open License (CPOL) 所有