ABP單元測試

1、介紹數據庫

在本文中,我將介紹如何爲基於ASP.NET Boilerplate的項目建立單元測試。 我將使用本文開發的相同的應用程序(使用AngularJs,ASP.NET MVC,Web API和EntityFramework來構建NLayered單頁面Web應用程序)而不是建立要測試的新應用程序。 解決方案結構就是這樣:架構

咱們將測試項目的應用服務。 它包括SimpleTaskSystem.Core,SimpleTaskSystem.Application和SimpleTaskSystem.EntityFramework項目。 您能夠閱讀本文,瞭解如何構建此應用程序。 在這裏,我將專一於測試。框架

參照項目:http://pan.baidu.com/s/1gf9xEU3異步

2、建立一個項目async

我建立了一個名爲SimpleTaskSystem.Test的新類庫項目,並添加了如下nuget包:ide

  • 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程序集的引用,由於咱們將測試這些項目。函數

2、準備一個基礎測試類單元測試

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。所以,測試之間是相互隔離的。ui

在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
{
        
}

此模塊僅定義依賴模塊,將進行測試。

3、建立第一個測試

咱們將建立第一個單元測試來測試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模式,包括三個步驟:

  1. Arrange(安排): 準備測試
  2. Act(行爲): 運行SUT(被測軟件 - 實際測試代碼)
  3. Assert(斷言): 檢查並驗證結果

在Should_Create_New_Tasks方法中,咱們將建立兩個任務,一個將被分配給Thomas More。 因此,咱們的三個步驟是:

  1. Arrange: 咱們從數據庫獲取該人(Thomas More),以獲取數據庫中的Id和當前任務數量(另外,咱們在構造函數中建立了TaskAppService)。
  2. Act: 咱們正在使用TaskAppService.CreateTask方法建立兩個任務。
  3. 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

我認爲第二個更容易和天然地寫和閱讀。 應該有不少其餘的擴展方法,使咱們的生活更輕鬆。 看到它的文檔。

4、測試異常

我想爲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基礎架構完成的。

5、在測試中使用存儲庫

我將測試從一我的到另外一我的分配一個任務:

        //試圖將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。 您可使用這些方法之一或混合。

6、測試異步方法

咱們也可使用xUnit來測試異步方法。 請參閱寫入以測試PersonAppService的GetAllPeople方法的方法。 GetAllPeople方法是異步的,因此測試方法也應該是異步的:

[Fact]
public async Task Should_Get_All_People()
{
    var output = await _personAppService.GetAllPeople();
    output.People.Count.ShouldBe(4);
}

7、概要

在本文中,我想顯示簡單的測試項目開發ASP.NET Boilerplate應用程序框架。 ASP.NET Boilerplate爲實現測試驅動開發提供了良好的基礎設施,或者簡單地爲您的應用程序建立了一些單元/集成測試。

Effort庫提供了一個與EntityFramework工做良好的假數據庫。 只要您使用EntityFramework和LINQ執行數據庫操做,它就能夠工做。 若是你有一個存儲過程而且要測試它,Effort不工做。 對於這種狀況,我建議使用LocalDB。

相關文章
相關標籤/搜索