程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> .NET網頁編程 >> 關於.NET >> 使用xUnit,EF,Effort和ABP進行單元測試(C#)

使用xUnit,EF,Effort和ABP進行單元測試(C#)

編輯:關於.NET

返回總目錄《一步一步使用ABP框架搭建正式項目系列教程》


本篇目錄

  • 介紹
  • 創建測試項目
  • 准備測試基類
  • 創建第一個測試
  • 測試異常
  • 在測試中使用倉儲
  • 測試異步方法
  • 小結

介紹

在這篇博客中,我們來說說基於ABP項目的單元測試。說到單元測試(Unit Test),估計很多人只有在上《軟件工程》這門課時才接觸過這個概念,平時寫代碼基本不寫測試的,測試的唯一辦法就是代碼寫完後跑一遍,看看符不符合預期的效果,如果符合就算完成任務了。但是,在大公司或者項目比較大(比如開發一個框架)的時候,單元測試很重要,它是保證軟件質量的一個重要指標。

在這篇博客中,我會在同一個解決方案中創建一個測試項目,而不是另外創建一個新的解決方案。解決方案的結構如下所示:

圖片

我將會測試該項目的應用服務,包括LcErp.Application,LcErp.Core,LcErp.EntityFramework子項目。至於如何使用ABP框架搭建項目,您可以參考之前的文章,本篇單講測試話題。

創建測試項目

如果你是用ABP啟動模板創建的項目,那麼它會自動創建測試項目的,否則,你可以手動創建一個測試項目。比如,我這裡創建了一個叫做LcErp.Tests的類庫項目,它位於Tests文件夾下。如果你是手動添加的類庫項目,請添加下面的nuget包:

  • Abp.TestBase:提供了一些使得測試基於ABP框架應用的測試更為簡單的基類。
  • Abp.EntityFramework:使用EF作為ORM。
  • Effort.EF6:使得創建一個偽造的、供EF容易使用的內存數據庫成為可能。
  • xunit:這是我們使用的測試框架。此外,也添加了在VS中運行測試的 xunit.runner.visualstudio
  • Shouldly:該包是為了方便書寫斷言的。

當我們添加了這些包之後,它們的依賴包也會自動添加到項目中。最後,我們要將LcErp.Application,LcErp.Core,LcErp.EntityFramework的引用添加到LcErp項目中,因為我們要測試這些項目。

圖片

准備測試基類

為了使創建測試類更簡單,我們要先創建一個基類,該基類准備了一個偽造的數據庫連接:

/// <summary>
    /// 這是我們所有測試類的基類。
    /// 它准備了ABP系統,模塊和一個偽造的內存數據庫。
    /// 具有初始數據的種子數據庫。
    /// 提供了容易使用的方法<see cref="LcErpDbContext"/>
    /// </summary>
public abstract class AppTestBase : AbpIntegratedTestBase
{
    protected AppTestBase()
        {
            //Seed initial data
            UsingDbContext(context =>
            {
                new InitialDbBuilder(context).Create();
                new TestDataBuilder(context).Create();
            });

            LoginAsDefaultTenantAdmin();
        }

    protected override void PreInitialize()
        {
            base.PreInitialize();

            //Fake DbConnection using Effort!
            LocalIocManager.IocContainer.Register(
                Component.For<DbConnection>()
                    .UsingFactoryMethod(DbConnectionFactory.CreateTransient)
                    .LifestyleSingleton()
                );
        }

    protected override void AddModules(ITypeList<AbpModule> modules)
        {
            base.AddModules(modules);

            //Adding testing modules. Depended modules of these modules are automatically added.
            modules.Add<LcErpTestModule>();
        }

    #region UsingDbContext

    protected void UsingDbContext(Action<LcErpDbContext> action)
        {
            using (var context = LocalIocManager.Resolve<LcErpDbContext>())
            {
                context.DisableAllFilters();
                action(context);
                context.SaveChanges();
            }
        }

    protected async Task UsingDbContextAsync(Action<LcErpDbContext> action)
        {
            using (var context = LocalIocManager.Resolve<LcErpDbContext>())
            {
                context.DisableAllFilters();
                action(context);
                await context.SaveChangesAsync();
            }
        }

    protected T UsingDbContext<T>(Func<LcErpDbContext, T> func)
        {
            T result;

            using (var context = LocalIocManager.Resolve<LcErpDbContext>())
            {
                context.DisableAllFilters();
                result = func(context);
                context.SaveChanges();
            }

            return result;
        }

    protected async Task<T> UsingDbContextAsync<T>(Func<LcErpDbContext, Task<T>> func)
        {
            T result;

            using (var context = LocalIocManager.Resolve<LcErpDbContext>())
            {
                context.DisableAllFilters();
                result = await func(context);
                await context.SaveChangesAsync();
            }

            return result;
        }

    #endregion

    ......這裡省略其他方法...

該基類繼承了AbpIntegratedTestBase,它是一個初始化了ABP系統的基類,定義了protected IIocManager LocalIocManager { get; }。每個測試都會使用這個專用的IIocManager。因此,測試之間是相互隔離的。

我們重寫了AddModules方法來添加我們想要測試的模塊(依賴的模塊會自動添加)。

在PreInitialize中,我們使用EffortDbConnection注冊到依賴注入系統中,注冊類型為Singleton。因此,即使我們在相同的測試中創建了不止一個DbContext,也會在一個測試中使用相同的數據庫(和連接)。為了使用該內存數據庫,LcErp必須有一個獲取DbConnection的構造函數。因此,數據庫上下文LcErp類中的構造函數會多一個,如下:

/* This constructor is used in tests to pass a fake/mock connection.
 */
public LcErpDbContext(DbConnection dbConnection)
    : base(dbConnection, true)
{

}

在AppTestBase的構造函數中,我們也在數據庫中創建了一個初始化數據(initial data)。這是很重要的,因為一些測試要求數據庫中存在的數據。InitialDbBuilder類填充數據庫的內容如下(詳細信息可自行查看項目):


public class InitialDbBuilder
{
    private readonly LcErpDbContext _context;

    public InitialDbBuilder(LcErpDbContext context)
    {
        _context = context;
    }

    public void Create()
    {
        _context.DisableAllFilters();

        new DefaultEditionCreator(_context).Create();
        new DefaultLanguagesCreator(_context).Create();
        new DefaultTenantRoleAndUserCreator(_context).Create();
        new DefaultSettingsCreator(_context).Create();

        _context.SaveChanges();
    }
}

AppTestBase的UsingDbContext方法使得當需要直接使用DbContext連接數據庫時創建DbContext更容易。在構造函數中我們使用了它,接下來我們將會在測試中看到如何使用它。

我們所有的測試類都會從AppTestBase繼承。因此,所有的測試都會通過初始化ABP啟動,使用一個具有初始化數據的偽造數據庫。為使測試更容易,我們也可以給這個基類添加通用的幫助方法。

創建第一個測試

接下來,我們正式創建第一個單元測試。下面的ProductionOrderAppService類中有一個CreateOrder方法,定義如下:

public class ProductionOrderAppService : LcErpAppServiceBase, IProductionOrderAppService
{
    private readonly IRepository<Order> _orderRepository;
    public ProductionOrderAppService(IRepository<Order> orderRepository)
    {
        _orderRepository = orderRepository;
    }

    public void CreateOrder(CreateOrderInput input)
    {
        var order = input.MapTo<Order>();//將dto對象映射為實體對象
        _orderRepository.Insert(order);
    }

    ......其他方法
}

一般來說,單元測試中,測試類的依賴是假的(通過使用一些模仿框架如Moq和NSubstitute來創建偽造的實現)。這使得單元測試更加困難,特別是當依賴逐漸增多時。

我們這裡不會這樣處理,因為我們使用了依賴注入,所有的依賴會通過具有真實實現的依賴注入自動填充,而不是偽造。我們偽造的東西只有數據庫。實際上,這是一個集成測試,因為它不僅測試了ProductionOrderAppService,還測試了倉儲,甚至我們測試了驗證,工作單元和ABP的其他基礎設施。這是非常具有價值的,因為我們正在更加真實地測試這個應用程序。

現在,我們開始創建第一個測試來測試CreateOrder 方法。

public class ProductionOrderAppService_Tests:AppTestBase
    {
        private readonly IProductionOrderAppService _orderAppService;

        public ProductionOrderAppService_Tests()
        {
            //創建被測試的類(SUT-Software Under Test[被測系統])
            _orderAppService = LocalIocManager.Resolve<IProductionOrderAppService>();
        }

        [Fact]
        public void Should_Create_New_Order()
        {
            //准備測試
            var initialCount = UsingDbContext(ctx => ctx.Orders.Count());

            //運行被測系統
            _orderAppService.CreateOrder(new CreateOrderInput
            {
                Amount = 10,
                CustomerId = 10,
                OrderId = "abc",
                OrderrDateTime = DateTime.Now,
                OrderUserId = 10,
                Sum = 10,
                Remark = "測試一" 
            });

            _orderAppService.CreateOrder(new CreateOrderInput
            {
                    OrderId = "efd",
                    Remark = "測試二"
            });

            //校驗結果

            UsingDbContext(ctx =>
            {
                ctx.Orders.Count().ShouldBe(initialCount+2);
                ctx.Orders.FirstOrDefault(o=>o.Remark=="測試一").ShouldNotBe(null);
                var order2 = ctx.Orders.FirstOrDefault(o => o.OrderId == "efd");
                order2.ShouldNotBe(null);
                order2.Remark.ShouldBe("測試二");
                //Assert.Equal("測試二",order2.Remark);

            });

        }

    }

正如之前所講,我們繼承了AppTestBase這個測試基類。在一個單元測試中,我們首先應該創建被測試的對象。在上面的構造函數中,使用LocalIocManager(依賴注入管理者)來創建了一個 IProductionOrderAppService(因為ProductionOrderAppService實現了IProductionOrderAppService,所以會創建ProductionOrderAppService)。通過這種方法,就避免了創建偽造的依賴實現。

Should_Create_New_Order是測試方法。它使用了xUnit的 Fact特性進行修飾。這樣,xUnit就理解了這是個測試方法,然後運行這個方法。

在一個測試方法中,我們一般遵循包含三步驟的AAA模式:

  1. Arrange:為測試准備
  2. Act:運行SUT(實際測試的代碼)
  3. Assert:校驗結果

在Should_Create_New_Order方法中,我們創建了2個訂單,因此,我們的三步驟是:

  1. Arrange:我們獲取數據庫中的訂單總數量
  2. Act:使用_orderAppService.CreateOrder添加了2個訂單
  3. Assert:檢查訂單數量是否增加了2個。同時嘗試從數據庫中獲取訂單,以檢查訂單是否被正確地插入到數據庫中。

這裡,我們使用了UsingDbContext方法來直接使用DbContext。如果測試成功,我們就知道了當輸入合理時,CreateOrder方法可以創建訂單。

要運行測試,我們要打開VS的測試管理器,選擇測試->窗口->測試資源管理器(如果沒有找到剛才創建的測試類和方法,先保存生成一下):

圖片
選中剛才創建的測試,右鍵“運行該測試”:
圖片

如上所示,我們的第一個單元測試通過了。恭喜恭喜!如果測試或者測試代碼不正確,那麼測試會失敗!

假設我注釋掉第二個訂單對象的Remark的賦值,然後再次運行測試,結果會失敗:

圖片

Shouldly類庫使得失敗信息更加清晰,也使得編寫斷言更加容易。比較一下xUnit的 Assert.Equal和 Shouldly的 ShouldBe擴展方法:

order2.Remark.ShouldBe("測試二");//使用Shouldly
Assert.Equal("測試二",order2.Remark);//使用xUnit的Assert
     

第一個讀寫更簡單且自然,並且Shouldly提供了很多其他的擴展方法來方便我們的編程,請查看Shouldly相應的文檔。

測試異常

我想為CreateOrder方法再創建一個測試方法,但是,這次輸入不合法

[Fact]
public void Should_Not_Create_New_Order_WithoutOrderId()
{
    Assert.Throws<AbpValidationException>(() => _orderAppService.CreateOrder(new CreateOrderInput 
    {
            Remark = "該訂單的OrderId沒有賦值"
    }));
}

如果沒有為創建的訂單的OrderId屬性賦值,那麼我期望CreateOrder會拋異常。因為在CreateOrderInput DTO類中,OrderId被標記為 Required,所以,如果CreateOrder拋出異常,測試就會成功,否則失敗。注意:驗證輸入和拋異常是ABP基礎設施處理的。

測試結果如下:

圖片

在測試中使???倉儲

下面在測試方法中使用倉儲,改造上面創建訂單的測試方法:

        [Fact]
        public void Should_Create_New_Order()
        {
            //准備測試
            //var initialCount = UsingDbContext(ctx => ctx.Orders.Count());
            //使用倉儲代替DbContext
            var orderRepo = LocalIocManager.Resolve<IRepository<Order>>();

            //運行被測系統
            _orderAppService.CreateOrder(new CreateOrderInput
            {
                Amount = 10,
                CustomerId = 10,
                OrderId = "abc",
                OrderrDateTime = DateTime.Now,
                OrderUserId = 10,
                Sum = 10,
                Remark = "測試一" 
            });

            _orderAppService.CreateOrder(new CreateOrderInput
            {
                    OrderId = "efd",
                    Remark = "測試二"
            });

            //校驗結果

            //UsingDbContext(ctx =>
            //{
            //    ctx.Orders.Count().ShouldBe(initialCount+2);
            //    ctx.Orders.FirstOrDefault(o=>o.Remark=="測試一").ShouldNotBe(null);
            //    var order2 = ctx.Orders.FirstOrDefault(o => o.OrderId == "efd");
            //    order2.ShouldNotBe(null);
            //    order2.Remark.ShouldBe("測試二");
            //    //Assert.Equal("測試二",order2.Remark);
            //});

            orderRepo.GetAll().Count().ShouldBe(2);

        }

測試異步方法

我們也可以使用xUnit測試異步方法。比如,ProductionOrderAppService的GetAllOrders方法是異步方法,那麼測試方法也應該是異步的(async)。

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

小結

這篇文章中,我只想展示一下基於ABP框架搭建的項目的測試。ABP提供了一個很好的基礎設施來實現測試驅動開發(TDD),或者為你的應用程序簡單地創建一些單元測試或集成測試。

Effort類庫提供了一個偽造的數據庫,它和EF協作地很好。只要你使用了EF或者Linq來執行數據庫操作,它就會工作。如果你使用了存儲過程,並想測試它,那麼Effort不支持。對於這些情況,建議使用LocalDB。

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved