.NET Core 3.0 單元測試與 Asp.Net Core 3.0 集成測試

單元測試與集成測試

測試必要性說明

相信大家在看到單元測試與集成測試這個標題時,會有很多感慨,我們無數次的在實踐中提到要做單元測試、集成測試,但是大多數項目都沒有做或者僅建了項目文件。這裡有客觀原因,已經接近交付日期了,我們沒時間做白盒測試了。也有主觀原因,面對業務複雜的代碼我們不知道如何入手做單元測試,不如就留給黑盒測試吧。但是,當我們的代碼無法進行單元測試的時候,往往就是代碼開始散發出壞味道的時候。長此以往,將欠下技術債務。在實踐過程中,技術債務常常會存在,關鍵在於何時償還,如何償還。

上圖說明了隨着時間的推移開發/維護難度的變化。

測試框架選擇

在 .NET Core 中,提供了 xUnit 、NUnit 、 MSTest 三種單元測試框架。

MSTest UNnit xUnit 說明 提示
[TestMethod] [Test] [Fact] 標記一個測試方法
[TestClass] [TestFixture] n/a 標記一個 Class 為測試類,xUnit 不需要標記特性,它將查找程序集下所有 Public 的類
[ExpectedException] [ExpectedException] Assert.Throws 或者 Record.Exception xUnit 去掉了 ExpectedException 特性,支持 Assert.Throws
[TestInitialize] [SetUp] Constructor 我們認為使用 [SetUp] 通常來說不好。但是,你可以實現一個無參構造器直接替換 [SetUp]。 有時我們會在多個測試方法中用到相同的變量,熟悉重構的我們會提取公共變量,並在構造器中初始化。但是,這裏我要強調的是:在測試中,不要提取公共變量,這會破壞每個測試用例的隔離性以及單一職責原則。
[TestCleanup] [TearDown] IDisposable.Dispose 我們認為使用 [TearDown] 通常來說不好。但是你可以實現 IDisposable.Dispose 以替換。 [TearDown] 和 [SetUp] 通常成對出現,在 [SetUp] 中初始化一些變量,則在 [TearDown] 中銷毀這些變量。
[ClassInitialize] [TestFixtureSetUp] IClassFixture< T > 共用前置類 這裏 IClassFixture< T > 替換了 IUseFixture< T > ,
[ClassCleanup] [TestFixtureTearDown] IClassFixture< T > 共用後置類 同上
[Ignore] [Ignore] [Fact(Skip=”reason”)] 在 [Fact] 特性中設置 Skip 參數以臨時跳過測試
[Timeout] [Timeout] [Fact(Timeout=n)] 在 [Fact] 特性中設置一個 Timeout 參數,當允許時間太長時引起測試失敗。注意,xUnit 的單位時毫秒。
[DataSource] n/a [Theory], [XxxData] Theory(數據驅動測試),表示執行相同代碼,但具有不同輸入參數的測試套件 這個特性可以幫助我們少寫很多代碼。

以上寫了 MSTest 、UNnit 、 xUnit 的特性以及比較,可以看出 xUnit 在使用上相對其它兩個框架來說提供更多的便利性。但是這裏最終實現還是看個人習慣以選擇。

單元測試

  1. 新建單元測試項目

  2. 新建 Class

  3. 添加測試方法

            /// <summary>
            /// 添加地址
            /// </summary>
            /// <returns></returns>
            [Fact]
            public async Task Add_Address_ReturnZero()
            {
                DbContextOptions<AddressContext> options = new DbContextOptionsBuilder<AddressContext>().UseInMemoryDatabase("Add_Address_Database").Options;
                var addressContext = new AddressContext(options);
    
                var createAddress = new AddressCreateDto
                {
                    City = "昆明",
                    County = "五華區",
                    Province = "雲南省"
                };
                var stubAddressRepository = new Mock<IRepository<Domain.Address>>();
                var stubProvinceRepository = new Mock<IRepository<Province>>();
                var addressUnitOfWork = new AddressUnitOfWork<AddressContext>(addressContext);
    
                var stubAddressService = new AddressServiceImpl.AddressServiceImpl(stubAddressRepository.Object, stubProvinceRepository.Object, addressUnitOfWork);
                await stubAddressService.CreateAddressAsync(createAddress);
                int addressAmountActual = await addressContext.Addresses.CountAsync();
                Assert.Equal(1, addressAmountActual);
            }
    • 測試方法的名字包含了測試目的、測試場景以及預期行為。
    • UseInMemoryDatabase 指明使用內存數據庫。
    • 創建 createAddress 對象。
    • 創建 Stub 。在單元測試中常常會提到幾個概念 Stub , Mock 和 Fake ,那麼在應用中我們該如何選擇呢?
      • Fake – Fake 通常被用於描述 Mock 或 Stub ,如何判斷它是 Stub 還是 Mock 依賴於使用上下文,換句話說,Fake 即是 Stub 也是 Mock 。
      • Stub – Stub 是系統中現有依賴項的可控替代品。通過使用 Stub ,你可以不用處理依賴直接測試你的代碼。默認情況下, 偽造對象以stub 開頭。
      • Mock – Mock 對象是系統中的偽造對象,它決定單元測試是否通過或失敗。Mock 會以 Fake 開頭,直到被斷言為止。
    • Moq4 ,使用 Moq4 模擬我們在項目中依賴對象。
  4. 打開視圖 -> 測試資源管理器。

  5. 點擊運行,得到測試結果。

  6. 至此,一個單元測試結束。

集成測試

集成測試確保應用的組件功能在包含應用的基礎支持下是正確的,例如:數據庫、文件系統、網絡等。

  1. 新建集成測試項目。

  2. 添加工具類 Utilities 。

    using System.Collections.Generic;
    using AddressEFRepository;
    
    namespace Address.IntegrationTest
    {
        public static class Utilities
        {
            public static void InitializeDbForTests(AddressContext db)
            {
                List<Domain.Address> addresses = GetSeedingAddresses();
                db.Addresses.AddRange(addresses);
                db.SaveChanges();
            }
    
            public static void ReinitializeDbForTests(AddressContext db)
            {
                db.Addresses.RemoveRange(db.Addresses);
                InitializeDbForTests(db);
            }
    
            public static List<Domain.Address> GetSeedingAddresses()
            {
                return new List<Domain.Address>
                {
                    new Domain.Address
                    {
                        City = "貴陽",
                        County = "測試縣",
                        Province = "貴州省"
                    },
                    new Domain.Address
                    {
                        City = "昆明市",
                        County = "武定縣",
                        Province = "雲南省"
                    },
                    new Domain.Address
                    {
                        City = "昆明市",
                        County = "五華區",
                        Province = "雲南省"
                    }
                };
            }
        }
    }
  3. 添加 CustomWebApplicationFactory 類,

     using System;
     using System.IO;
     using System.Linq;
     using AddressEFRepository;
     using Microsoft.AspNetCore.Hosting;
     using Microsoft.AspNetCore.Mvc.Testing;
     using Microsoft.EntityFrameworkCore;
     using Microsoft.Extensions.Configuration;
     using Microsoft.Extensions.DependencyInjection;
     using Microsoft.Extensions.Logging;
    
     namespace Address.IntegrationTest
     {
         public class CustomWebApplicationFactory<TStartup> : WebApplicationFactory<TStartup> where TStartup : class
         {
             protected override void ConfigureWebHost(IWebHostBuilder builder)
             {
                 string projectDir = Directory.GetCurrentDirectory();
                 string configPath = Path.Combine(projectDir, "appsettings.json");
                 builder.ConfigureAppConfiguration((context, conf) =>
                 {
                     conf.AddJsonFile(configPath);
                 });
    
                 builder.ConfigureServices(services =>
                 {
                     ServiceDescriptor descriptor = services.SingleOrDefault(d => d.ServiceType == typeof(DbContextOptions<AddressContext>));
    
                     if (descriptor != null)
                     {
                         services.Remove(descriptor);
                     }
    
                     services.AddDbContextPool<AddressContext>((options, context) =>
                     {
                         //var configuration = options.GetRequiredService<IConfiguration>();
                         //string connectionString = configuration.GetConnectionString("TestAddressDb");
                         //context.UseMySql(connectionString);
                         context.UseInMemoryDatabase("InMemoryDbForTesting");
    
                     });
    
                     // Build the service provider.
                     ServiceProvider sp = services.BuildServiceProvider();
                     // Create a scope to obtain a reference to the database
                     // context (ApplicationDbContext).
                     using IServiceScope scope = sp.CreateScope();
                     IServiceProvider scopedServices = scope.ServiceProvider;
                     var db = scopedServices.GetRequiredService<AddressContext>();
                     var logger = scopedServices.GetRequiredService<ILogger<CustomWebApplicationFactory<TStartup>>>();
    
                     // Ensure the database is created.
                     db.Database.EnsureCreated();
    
                     try
                     {
                         // Seed the database with test data.
                         Utilities.ReinitializeDbForTests(db);
                     }
                     catch (Exception ex)
                     {
                         logger.LogError(ex, "An error occurred seeding the " + "database with test messages. Error: {Message}", ex.Message);
                     }
                 });
             }
         }
     }
    • 這裏為什麼要添加 CustomWebApplicationFactory 呢?
      WebApplicationFactory 是用於在內存中引導應用程序進行端到端功能測試的工廠。通過引入自定義 CustomWebApplicationFactory 類重寫 ConfigureWebHost 方法,我們可以重寫我們在 StartUp 中定義的內容,換句話說我們可以在測試環境中使用正式環境的配置,同時可以重寫,例如:數據庫配置,數據初始化等等。
    • 如何準備測試數據?
      我們可以使用數據種子的方式加入數據,數據種子可以針對每個集成測試做數據準備。
    • 除了內存數據庫,還可以使用其他數據庫進行測試嗎?
      可以。
  4. 添加集成測試 AddressControllerIntegrationTest 類。

     using System.Collections.Generic;
     using System.Linq;
     using System.Net.Http;
     using System.Threading.Tasks;
     using Address.Api;
     using Microsoft.AspNetCore.Mvc.Testing;
     using Newtonsoft.Json;
     using Xunit;
    
     namespace Address.IntegrationTest
     {
         public class AddressControllerIntegrationTest : IClassFixture<CustomWebApplicationFactory<Startup>>
         {
             public AddressControllerIntegrationTest(CustomWebApplicationFactory<Startup> factory)
             {
                 _client = factory.CreateClient(new WebApplicationFactoryClientOptions
                 {
                     AllowAutoRedirect = false
                 });
             }
    
             private readonly HttpClient _client;
    
             [Fact]
             public async Task Get_AllAddressAndRetrieveAddress()
             {
                 const string allAddressUri = "/api/Address/GetAll";
                 HttpResponseMessage allAddressesHttpResponse = await _client.GetAsync(allAddressUri);
    
                 allAddressesHttpResponse.EnsureSuccessStatusCode();
    
                 string allAddressStringResponse = await allAddressesHttpResponse.Content.ReadAsStringAsync();
                 var addresses = JsonConvert.DeserializeObject<IList<AddressDto.AddressDto>>(allAddressStringResponse);
                 Assert.Equal(3, addresses.Count);
    
                 AddressDto.AddressDto address = addresses.First();
                 string retrieveUri = $"/api/Address/Retrieve?id={address.ID}";
                 HttpResponseMessage addressHttpResponse = await _client.GetAsync(retrieveUri);
    
                 // Must be successful.
                 addressHttpResponse.EnsureSuccessStatusCode();
    
                 // Deserialize and examine results.
                 string addressStringResponse = await addressHttpResponse.Content.ReadAsStringAsync();
                 var addressResult = JsonConvert.DeserializeObject<AddressDto.AddressDto>(addressStringResponse);
                 Assert.Equal(address.ID, addressResult.ID);
                 Assert.Equal(address.Province, addressResult.Province);
                 Assert.Equal(address.City, addressResult.City);
                 Assert.Equal(address.County, addressResult.County);
             }
         }
     }
  5. 在測試資源管理器中運行集成測試方法。

  6. 結果。

  7. 至此,集成測試完成。需要注意的是,集成測試往往耗時比較多,所以建議能使用單元測試時就不要使用集成測試。

總結:當我們寫單元測試時,一般不會同時存在 Stub 和 Mock 兩種模擬對象,當同時出現這兩種對象時,表明單元測試寫的不合理,或者業務寫的太過龐大,同時,我們可以通過單元測試驅動業務代碼重構。當需要重構時,我們應盡量完成重構,不要留下欠下過多技術債務。集成測試有自身的複雜度存在,我們不要節約時間而打破單一職責原則,否則會引發不可預期後果。為了應對業務修改,我們應該在業務修改以後,進行回歸測試,回歸測試主要關注被修改的業務部分,同時測試用例如果有沒要可以重寫,運行整個和修改業務有關的測試用例集。

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※為什麼 USB CONNECTOR 是電子產業重要的元件?

網頁設計一頭霧水??該從何著手呢? 找到專業技術的網頁設計公司,幫您輕鬆架站!

※想要讓你的商品成為最夯、最多人討論的話題?網頁設計公司讓你強力曝光

※想知道最厲害的台北網頁設計公司推薦台中網頁設計公司推薦專業設計師”嚨底家”!!