一、介紹
在本文中,我將介紹如何為基於ASP.NET Boilerplate的項目創建單元測試。 我將使用本文開發的相同的應用程序(使用AngularJs,ASP.NET MVC,Web API和EntityFramework來構建NLayered單頁面Web應用程序)而不是創建要測試的新應用程序。 解決方案結構就是這樣:
我們將測試項目的應用服務。 它包括SimpleTaskSystem.Core,SimpleTaskSystem.Application和SimpleTaskSystem.EntityFramework項目。 您可以閱讀本文,了解如何構建此應用程序。 在這里,我將專注於測試。
參照項目:http://pan.baidu.com/s/1gf9xEU3
二、創建一個項目
我創建了一個名為SimpleTaskSystem.Test的新類庫項目,並添加了以下nuget包:
- Abp.TestBase: 提供一些基類,使基於ABP的項目更容易測試。
- Abp.EntityFramework: 我們使用EntityFramework 6.x作為ORM。
- Effort.EF6: 可以為易於使用的EF創建一個假的,內存中的數據庫。
- xunit: 我們將使用的測試框架。 另外,添加了xunit.runner.visualstudio包以在Visual Studio中運行測試。 當我寫這篇文章時,這個包是預先釋放的。 所以,我在nuget包管理器對話框中選擇了'Include Prerelease'。
- Shouldly: 此庫容易編寫斷言。
- xunit.runner.visualstudio: 不安裝此庫,發現不了測試方法
當我們添加這些包時,它們的依賴關系也將被自動添加。 最后,我們應該添加對SimpleTaskSystem.Application,SimpleTaskSystem.Core和SimpleTaskSystem.EntityFramework程序集的引用,因為我們將測試這些項目。
二、准備一個基礎測試類
1,為了更容易地創建測試類,我將創建一個准備假數據庫連接的基類:
/// <summary> /// 這是所有測試類的基礎類。 /// 它准備了ABP系統,模塊和一個偽造的內存數據庫。 /// 具有初始數據的種子數據庫(<see cref =“SimpleTaskSystemInitialDataBuilder”/>)。 /// 提供使用DbContext輕松使用的方法。 /// </summary> public abstract class SimpleTaskSystemTestBase : AbpIntegratedTestBase<SimpleTaskSystemTestModule> { protected SimpleTaskSystemTestBase() { //種子初始數據 UsingDbContext(context => new SimpleTaskSystemInitialDataBuilder().Build(context)); } protected override void PreInitialize() { //假DbConnection使用Effort! LocalIocManager.IocContainer.Register( Component.For<DbConnection>() .UsingFactoryMethod(Effort.DbConnectionFactory.CreateTransient) .LifestyleSingleton() ); base.PreInitialize(); } public void UsingDbContext(Action<SimpleTaskSystemDbContext> action) { using (var context = LocalIocManager.Resolve<SimpleTaskSystemDbContext>()) { context.DisableAllFilters(); action(context); context.SaveChanges(); } } public T UsingDbContext<T>(Func<SimpleTaskSystemDbContext, T> func) { T result; using (var context = LocalIocManager.Resolve<SimpleTaskSystemDbContext>()) { context.DisableAllFilters(); result = func(context); context.SaveChanges(); } return result; } }
該基類繼承了AbpIntegratedTestBase,它是一個初始化了ABP系統的基類,定義了 protected IIocManager LocalIocManager { get; } 。每個測試都會使用這個專用的IIocManager。因此,測試之間是相互隔離的。
在SimpleTaskSystemTestBase的PreInitialize方法中,我們正在使用Effort注冊DbConnection到依賴注入系統(PreInitialize方法用於運行一些代碼,僅用於ABP初始化)。 我們將其注冊為Singleton(用於LocalIocConainer)。 因此,即使我們在同一測試中創建了多個DbContext,測試中也將使用相同的數據庫(和連接)。
SimpleTaskSystemTestBase的UsingDbContext方法使得當我們需要直接使用DbContect來處理數據庫時,可以更容易地創建DbContextes。 在構造函數中,我們使用它。 另外,我們將在測試中看到如何使用它。
SimpleTaskSystemDbContext必須具有獲取DbConnection的構造函數才能使用該內存數據庫。 所以,我添加下面的構造函數接受一個DbConnection:
public class SimpleTaskSystemDbContext : AbpDbContext { public virtual IDbSet<Task> Tasks { get; set; } public virtual IDbSet<Person> People { get; set; } public SimpleTaskSystemDbContext() : base("Default") { } public SimpleTaskSystemDbContext(string nameOrConnectionString) : base(nameOrConnectionString) { } //這個構造函數用於測試 public SimpleTaskSystemDbContext(DbConnection connection) : base(connection, true) { } }
在SimpleTaskSystemTestBase的構造函數中,我們還在數據庫中創建一個初始數據。 這很重要,因為一些測試需要數據庫中存在的數據。 SimpleTaskSystemInitialDataBuilder類填充數據庫,如下所示:
public class SimpleTaskSystemInitialDataBuilder { public void Build(SimpleTaskSystemDbContext context) { //添加一些人 context.People.AddOrUpdate( p => p.Name, new Person {Name = "Isaac Asimov"}, new Person {Name = "Thomas More"}, new Person {Name = "George Orwell"}, new Person {Name = "Douglas Adams"} ); context.SaveChanges(); //添加一些任務 context.Tasks.AddOrUpdate( t => t.Description, new Task { Description = "my initial task 1" }, new Task { Description = "my initial task 2", State = TaskState.Completed }, new Task { Description = "my initial task 3", AssignedPerson = context.People.Single(p => p.Name == "Douglas Adams") }, new Task { Description = "my initial task 4", AssignedPerson = context.People.Single(p => p.Name == "Isaac Asimov"), State = TaskState.Completed }); context.SaveChanges(); } }
我們所有的測試類都將從SimpleTaskSystemTestBase繼承。 因此,所有測試都將通過使用具有初始數據的假數據庫初始化ABP來啟動。 我們還可以為此基類添加常用的幫助方法,以便使測試更容易。
2,我們應該創建一個專門用於測試的模塊。 這是SimpleTaskSystemTestModule在這里:
[DependsOn( typeof(SimpleTaskSystemDataModule), typeof(SimpleTaskSystemApplicationModule) )] public class SimpleTaskSystemTestModule : AbpModule { }
此模塊僅定義依賴模塊,將進行測試。
三、創建第一個測試
我們將創建第一個單元測試來測試TaskAppService類的CreateTask方法。
TaskAppService類和CreateTask方法定義如下:
public class TaskAppService : ApplicationService, ITaskAppService { private readonly ITaskRepository _taskRepository; private readonly IRepository<Person> _personRepository; public TaskAppService(ITaskRepository taskRepository, IRepository<Person> personRepository) { _taskRepository = taskRepository; _personRepository = personRepository; } public void CreateTask(CreateTaskInput input) { Logger.Info("Creating a task for input: " + input); var task = new Task { Description = input.Description }; if (input.AssignedPersonId.HasValue) { task.AssignedPerson = _personRepository.Load(input.AssignedPersonId.Value); } _taskRepository.Insert(task); } //...other methods }
我們先創建一個測試來測試CreateTask方法。
public class TaskAppService_Tests : SimpleTaskSystemTestBase { private readonly ITaskAppService _taskAppService; public TaskAppService_Tests() { //創建被測試的類(SUT(Software Under Test) - 被測系統) _taskAppService = LocalIocManager.Resolve<ITaskAppService>(); } [Fact] public void Should_Create_New_Tasks() { //准備測試 var initialTaskCount = UsingDbContext(context => context.Tasks.Count()); var thomasMore = GetPerson("Thomas More"); //運行SUT _taskAppService.CreateTask( new CreateTaskInput { Description = "my test task 1" }); _taskAppService.CreateTask( new CreateTaskInput { Description = "my test task 2", AssignedPersonId = thomasMore.Id }); //檢查結果 UsingDbContext(context => { context.Tasks.Count().ShouldBe(initialTaskCount + 2); context.Tasks.FirstOrDefault(t => t.AssignedPersonId == null && t.Description == "my test task 1").ShouldNotBe(null); var task2 = context.Tasks.FirstOrDefault(t => t.Description == "my test task 2"); task2.ShouldNotBe(null); task2.AssignedPersonId.ShouldBe(thomasMore.Id); }); } private Person GetPerson(string name) { return UsingDbContext(context => context.People.Single(p => p.Name == name)); } }
如前所述,我們從SimpleTaskSystemTestBase繼承。 在單元測試中,我們應該創建要測試的對象。 在構造函數中,我使用LocalIocManager(依賴注入管理器)來創建一個ITaskAppService(它創建了TaskAppService,因為它實現了ITaskAppService)。 以這種方式,我擺脫了創建依賴關系的模擬實現。
Should_Create_New_Tasks是測試方法。 它使用xUnit的Fact屬性進行裝飾。 因此,xUnit了解這是一種測試方法,它運行該方法。
在測試方法中,我們通常遵循AAA模式,包括三個步驟:
- Arrange(安排): 准備測試
- Act(行為): 運行SUT(被測軟件 - 實際測試代碼)
- Assert(斷言): 檢查並驗證結果
在Should_Create_New_Tasks方法中,我們將創建兩個任務,一個將被分配給Thomas More。 所以,我們的三個步驟是:
- Arrange: 我們從數據庫獲取該人(Thomas More),以獲取數據庫中的Id和當前任務數量(另外,我們在構造函數中創建了TaskAppService)。
- Act: 我們正在使用TaskAppService.CreateTask方法創建兩個任務。
- Assert: 我們正在檢查任務計數是否增加2.我們還嘗試從數據庫獲取創建的任務,以查看它們是否正確插入數據庫。
在這里,UsingDbContext方法可以幫助我們直接使用DbContext。 如果此測試成功,我們了解如果我們提供有效的輸入,CreateTask方法可以創建任務。 此外,存儲庫正在工作,因為它將Tasks插入數據庫。
要運行測試,我們通過選擇TEST \ Windows \ Test Explorer打開Visual Studio測試資源管理器:
然后我們點擊測試資源管理器中的“全部運行”鏈接。 它在解決方案中找到並運行所有測試:
如上所示,我們的第一個單元測試通過。恭喜! 如果測試或測試代碼不正確,測試將失敗。 假設我們已經忘記將賦值賦給給某人(要測試它,注釋掉TaskAppService.CreateTask方法中的相關行)。 當我們運行測試時,它將失敗:
Shouldly庫使得失敗的消息更清晰。 它也使寫入斷言變得容易。 比較xUnit的Assert.Equal與Shouldly的ShouldBe擴展方法:
Assert.Equal(thomasMore.Id, task2.AssignedPersonId); //Using xunit's Assert task2.AssignedPersonId.ShouldBe(thomasMore.Id); //Using Shouldly
我認為第二個更容易和自然地寫和閱讀。 應該有很多其他的擴展方法,使我們的生活更輕松。 看到它的文檔。
四、測試異常
我想為CreateTask方法創建第二個測試。 但是,這次輸入無效:
[Fact] public void Should_Not_Create_Task_Without_Description() { //說明未設置 Assert.Throws<AbpValidationException>(() => _taskAppService.CreateTask(new CreateTaskInput())); }
我希望CreateTask方法拋出AbpValidationException,如果我沒有設置描述創建任務。 因為在CreateTaskInput DTO類中將描述屬性標記為必需(請參閱源代碼)。 如果CreateTask引發異常,則此測試成功,否則失敗。 注意; 驗證輸入和拋出異常是由ASP.NET Boilerplate基礎架構完成的。
五、在測試中使用存儲庫
我將測試從一個人到另一個人分配一個任務:
//試圖將Isaac Asimov的任務分配給Thomas More [Fact] public void Should_Change_Assigned_People() { //我們可以使用存儲庫而不是DbContext var taskRepository = LocalIocManager.Resolve<ITaskRepository>(); //獲取測試數據 var isaacAsimov = GetPerson("Isaac Asimov"); var thomasMore = GetPerson("Thomas More"); var targetTask = taskRepository.FirstOrDefault(t => t.AssignedPersonId == isaacAsimov.Id); targetTask.ShouldNotBe(null); //運行 SUT _taskAppService.UpdateTask( new UpdateTaskInput { TaskId = targetTask.Id, AssignedPersonId = thomasMore.Id }); //檢查結果 taskRepository.Get(targetTask.Id).AssignedPersonId.ShouldBe(thomasMore.Id); }
在這個測試中,我使用ITaskRepository執行數據庫操作,而不是直接使用DbContext。 您可以使用這些方法之一或混合。
六、測試異步方法
我們也可以使用xUnit來測試異步方法。 請參閱寫入以測試PersonAppService的GetAllPeople方法的方法。 GetAllPeople方法是異步的,所以測試方法也應該是異步的:
[Fact] public async Task Should_Get_All_People() { var output = await _personAppService.GetAllPeople(); output.People.Count.ShouldBe(4); }
七、概要
在本文中,我想顯示簡單的測試項目開發ASP.NET Boilerplate應用程序框架。 ASP.NET Boilerplate為實現測試驅動開發提供了良好的基礎設施,或者簡單地為您的應用程序創建了一些單元/集成測試。
Effort庫提供了一個與EntityFramework工作良好的假數據庫。 只要您使用EntityFramework和LINQ執行數據庫操作,它就可以工作。 如果你有一個存儲過程並且要測試它,Effort不工作。 對於這種情況,我建議使用LocalDB。