日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

技术分享|单元测试推广与实战-在全新的DDD架构上进行单元测试

發布時間:2023/12/4 编程问答 42 豆豆
生活随笔 收集整理的這篇文章主要介紹了 技术分享|单元测试推广与实战-在全新的DDD架构上进行单元测试 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.


源寶導讀:單元測試是伴隨軟件工程出現和發展的,怎么做大家可能各有見解。本文介紹了單元測試中的反模式,強調了可測試性的重要性,并以 DDD 架構項目的迭代進程作為示例,演示了單元測試的組織過程,展示了單元測試如何影響架構設計,進而提高交付質量。

一、背景

? ? 為了滿足日益增長的業務需求,天際-DevOps平臺于近期開始了重構工作。由于重構過程對各業務場景進行了重新定義,開發過程推行基于 DDD 的編程架構,所以是推廣和落地單元測試的很好時機。糧草未動,兵馬先行,這里先介紹單元測試的定義、必要性,接著是引入 dotnet 單元測試相關的知識,然后以反模式示例演示可測試性概念,最后在全新項目上演示整個迭代周期中單元測試的策略與實現細節,并進行小結。

二、單元測試相關的概念、工具和技巧

2.1、單元測試的定義

? ? 單元測試是指對軟件中的最小可測試單元進行檢查和驗證,是最低級別的測試活動。開發者編寫的一小段代碼,用于檢驗被測代碼的一個很小的、很明確的功能是否正確。通常而言,一個單元測試是用于判斷某個特定條件(或者場景)下某個特定函數的行為。

  • 驗證代碼與設計相符合;

  • 跟蹤需求與設計的實現;

  • 發現設計和需求中存在的缺陷;

  • 發現在編碼過程中引入的錯誤。

2.2、單元測試的必要性

單元測試能在開發階段發現 BUG,及早暴露,收益高,是交付質量的保證。

  • 來自微軟的統計數據顯示,bug在單元測試階段被發現,平均耗時3.25小時,如果漏到系統測試階段,要花費11.5小時。

  • 85% 的缺陷都在代碼設計階段產生,而發現 bug 的階段越靠后,耗費成本就越高,指數級別的增高。

2.3、單元測試相關的模式、知識點和工具

Arrange-Act-Assert (AAA) 模式

? ? AAA(準備、執行、斷言)模式是編寫待測試方法的單元測試的常用方法。

? ? 一個典型的單元測試用例如下:

[Fact] public void Add_EmptyString_ReturnsZero() {// Arrangevar stringCalculator = new StringCalculator();// Actvar actual = stringCalculator.Add("");// AssertAssert.Equal(0, actual); }

NSubstitute

? ? 該類庫對自身的定位是?A friendly substitute for .NET mocking libraries,作為老牌 mock 庫 moq 的替代實現。(mock 離不開動態代理,NSubstitute 依賴 Castle Core,其原理另起篇幅描述。)

// Arrange(準備):Prepare var calculator = Substitute.For<ICalculator>();// Act(執行):Set a return value calculator.Add(1, 2).Returns(3); Assert.AreEqual(3, calculator.Add(1, 2));// Assert(斷言 ):Check received calls calculator.Received().Add(1, Arg.Any<int>()); calculator.DidNotReceive().Add(2, 2);

使用InternalsVisible ToAttribute測試內部類

? ? 為了避免暴露大量的實現細節、提高內聚性,開發人員應應減少?public?訪問修飾符的使用。但是非公開的類和方法如何進行測試?這就是?InternalsVisible ToAttribute 的作用,我們可以在被測項目的AssemblyInfo.cs?文件中添加定義,該特性接受 assembly 名稱作為參數,對其暴露內部可見性。

[assembly: InternalsVisibleTo("XXX.Tests")]

? ? 也可以在被測試目標的項目文件?.csproj?中使用,并支持使用項目的上下文變量作為參數名。

<ItemGroup><AssemblyAttribute Include="System.Runtime.CompilerServices.InternalsVisibleTo"><_Parameter1>$(MSBuildProjectName).Tests</_Parameter1></AssemblyAttribute></ItemGroup>

? ? 通過以上兩種方式,單元測試項目擁有了對被測試項目中?internal?類和方法的訪問能力。

擴展方法的測試

? ? 大多數場景下擴展方法不具備可測試性,efcore 中以?Async?結尾的擴展方法,測試它們需要實現?IDbAsync QueryProvider?接口,步驟繁瑣,業務實現中應注意擴展方法的可測試性。

2.4、可測試性

? ? 可測試性的回顧仍然十分有必要,大概上可以歸于以下三類。

不確定性/未決行為

// BAD public class PowerTimer {public String GetMeridiem(){var time = DateTime.Now;if (time.Hour >= 0 && time.Hour < 12){return "AM";}return "PM";} }

依賴于實現:不可 mock

// BAD: 依賴于實現 public class DepartmentService {private CacheManager _cacheManager = new CacheManager();public List<Department> GetDepartmentList(){List<Department> result;if (_cacheManager.TryGet("department-list", out result)){return result;}// ... do stuff } }// BAD: 靜態方法 public static bool CheckNodejsInstalled() {return Environment.GetEnvironmentVariable("PATH").Contains("nodejs", StringComparison.OrdinalIgnoreCase); }

復雜繼承/高耦合代碼:測試困難

? ? 隨著步驟與流程判斷增加,場景組合和 mock 工作量成倍堆積,直到不可測試。

三、實戰:在全新的 DDD 架構上進行單元測試

? ? HelloDevCloud?是一個假想的早期 devOps 產品,提供了組織(Organization)和項目(Project)管理,遵從極簡的 DDD 架構,預計的項目結構如下:

$ tree -L 2 . ├── doc ├── HelloDevCloud.sln ├── README.md ├── src │ ├── HelloDevCloud.Domain 領域對象 │ ├── HelloDevCloud.Domain.Shared │ ├── HelloDevCloud.DomainService 領域服務 │ ├── HelloDevCloud.EntityFrameworkCore 基于 efcore 的倉儲模式實現 │ ├── HelloDevCloud.Infrastructure 基礎設施 │ ├── HelloDevCloud.Repositories DbContext 與倉儲 │ └── HelloDevCloud.Web Web 接口 └── test├── HelloDevCloud.DomainService.Tests 領域服務測試用例├── HelloDevCloud.RepositoriesTests DbContext 與倉儲測試用例└── HelloDevCloud.Web.Tests Web 接口測試用例

? ??基于 DDD 分層架構不一而足,本示例用作單元測試演示。

? ? 目前已有如下領域劃分:

  • 每個組織(Organization)都可以創建一個或多個項目(Project);

  • 提供公共的 GitLab 用于托管代碼,每個項目(Project)創建之時有 master 和 develop 分支被創建出來;

  • 項目(Project)目前支持公共 GitLab,但預備在將來支持私有 GitLab。

  • 3.1、需求-迭代1:分支管理

    ? ? 本迭代預計引入分支管理功能:

  • 每個項目(Project,聚合根)都能創建特定類別的分支(Branch,實體),目前支持特性分支(feature)和修復分支(hotfix),分別從 develop 分支和 master 分支簽出;

  • GitLab 有自己的管理入口,分支創建時需要檢查項目和分支是否存在;

  • 分支創建成功后將提交記錄(Commit)寫入分支。

  • 前期:分析調用時序

    前期:設計模塊與依賴關系

    • IProjectService:領域服務,依賴IGitlabClient完成業務驗證與調用;

    • IProjectRepository:項目(Project,聚合根)倉儲,更新聚合根;

    • IBranchRepository:分支(Branch,實體)倉儲,檢查;

    • IGitlabClient:基礎設施。

    前期:列舉單元測試用例

    • 項目領域服務:

      • 在 GitLab 項目不存在時斷言失敗:CreateBranch_WhenRemoteProjectNotExist_ShouldFailed()

      • 在 GitLab 分支已經存在時斷言失敗:CreateBranch_WhenRemoteBranchPresented_ShouldFailed()

      • 創建不支持的特性分支時斷言失敗:CreateBranch_UseTypeNotSupported_ShouldFailed()

      • 正確創建的分支應包含提交記錄(Commit):CreateBranch_WhenParamValid_ShouldQuoteCommit()

    • 項目應用服務:

      • 在項目(Project)不存在時斷言失敗:Post_WhenProjectNotExist_ShouldFail()

      • 在項目(Project)不存在時斷言失敗:Post_WhenProjectNotExist_ShouldFail()

      • 參數合法時返回預期的分支的簽出結果:Post_WhenParamValid_ShouldCreateBranch()

    中期:業務邏輯實現

  • 項目(Project )作為聚合根添加分支(Branch)作為組成。

  • 我們總是需要在遠程與本地項目、分支之前進行檢查,它們由領域服務組織。

  • public async Task<Branch> CreateBranch(Project project, string branchName, BranchType branchType) {var gitProject = await _gitlabClient.Projects.GetAsync(project.Gitlab.Id);// 斷言遠程項目存在if (gitProject == null){throw new NotImplementedException("project should existed");}// 斷言遠程分支不何存在var gitBranch = await _gitlabClient.Branches.GetAsync(project.Gitlab.Id, branchName);if (gitBranch != null){throw new ArgumentOutOfRangeException(nameof(branchName), "remote branch already existed");}// 獲取簽出分支var reference = GetBranchReferenceForCreate(branchType);var request = new CreateBranchRequest(branchName, reference);// 創建分支gitBranch = await _gitlabClient.Branches.CreateAsync(project.Gitlab.Id, request);return project.CheckoutBranch(gitBranch.Name, gitBranch.Commit.Id, branchType); }private String GetBranchReferenceForCreate(BranchType branchType) {return branchType switch{BranchType.Feature => Branch.Develop,BranchType.Hotfix => Branch.Master,_ => throw new ArgumentOutOfRangeException(nameof(branchType), $"Not supported branchType {branchType}"),}; }

    中期:單元測試實現

    • 領域服務:測試用例見于項目源碼?test/HelloDevCloud.DomainService.Tests /Projects/ProjectServiceTest.cs。

    • 應用服務:測試用例見于項目源碼?test/HelloDevCloud.Web.Tests/Controllers /ProjectControllerTest.cs。

    實戰小結

  • 單元測試用例體現了業務規則;

  • 單元測試同架構一樣是分層的。

  • 3.2、需求-迭代2:支持外部 GitLab,支持分支搜索

    本迭代預期添加以下內容:

  • 支持使用外部 GitLab 上管理分支;

  • 并支持使用名稱搜索組織下的分支列表。

  • 前期:設計模塊與依賴關系

    前期:列舉單元測試用例

    • 項目領域服務:

  • 使用外部 GitLab 倉庫能簽出分支:CreateBranch_UserExternalRepository _ShouldQuoteCommit();

    • 分支倉儲:

    • 從配置了外部倉庫的項目獲取分支應返回符合預期的結果:GetAllByOrganization_ ViaName_ReturnMatched

    • 中期:業務邏輯實現

    • 使用新的工廠接口 IGitlabClient Factory 替換 IGitlabClient;

    • 使用組織 Id 查詢分支列表。

    • public IList<Branch> GetAllByOrganization(int organizationId, string search) {var projects = EfUnitOfWork.DbSet<Project>();var branchs = EfUnitOfWork.DbSet<Branch>();var query = from b in branchsjoin p in projectson b.ProjectId equals p.Idwhere p.OrganizationId == organizationId && (b.Type == BranchType.Feature || b.Type == BranchType.Hotfix)select b;if (string.IsNullOrWhiteSpace(search) == false){query.Where(x => x.Name.Contains(search));}return query.ToArray(); }

      中期:單元測試實現

      • 領域服務:測試用例見于項目源碼?test/HelloDevCloud.DomainService.Tests/ Projects/ProjectServiceTest.cs;

      • 倉儲實現:測試用例見于項目源碼?test/HelloDevCloud.RepositoriesTests/ Implements/BranchRepositoryTest.cs。

      ? ? 注意:倉儲仍然是可測且應該進行測試的,mock 數據庫查詢的主要工作是 mock IQuerable<T>,但是 mock 數據庫讀寫并不容易。好在 efcore 提供了 UseInMemory Database()?模式,無須我們再提供 FackRepository 一類實現。

      [Fact] public void GetAllByOrganization_ViaName_ReturnMatched() {var options = new DbContextOptionsBuilder<DevCloudContext>().UseInMemoryDatabase("DevCloudContext").Options;using var devCloudContext = new DevCloudContext(options);devCloudContext.Set<Project>().AddRange(new[] {new Project{Id = 11,Name = "成本系統",OrganizationId = 1},new Project{Id = 12,Name = "成本系統合同執行應用",OrganizationId = 1},new Project{Id = 13,Name = "售樓系統",OrganizationId = 2},});devCloudContext.Set<Branch>().AddRange(new[] {new Branch{Id = 101,Name = "3.0.20.4_core分支",ProjectId = 11,Type = BranchType.Feature},new Branch{Id = 102,Name = "3.0.20.1_core發版修復分支15",ProjectId = 12,Type = BranchType.Hotfix},new Branch{Id = 103,Name = "730Core自動化驗證",ProjectId = 13,Type = BranchType.Feature}});devCloudContext.SaveChanges();var unitOfWork = new EntityFrameworkUnitOfWork(devCloudContext);var branchRepo = new BranchRepository(unitOfWork);var branches = branchRepo.GetAllByOrganization(1, "core");Assert.Equal(2, branches.Count);Assert.Equal(101, branches[0].Id);Assert.Equal(102, branches[1].Id); }

      ANTI-PATTERN:依賴具體實現

      ? ? 支持外部 GitLab 倉庫需要動態生成 IGitlabClient 實例,故在業務邏輯中根據項目(Project)設置實例化 GitlabClinet是很“自然”的事情,但代碼不再具有可測試性。

      ? ? 對應的實現邏輯片段如下:

      //BAD - private readonly IGitLabClient _gitlabClient; + private readonly IOptions<GitlabOptions> _gitlabOptions;- public ProjectService(IGitLabClient gitlabClient) + public ProjectService(IOptions<GitlabOptions> gitlabOptions){ - _gitlabClient = gitlabClient; + _gitlabOptions = gitlabOptions;}public async Task<Branch> CreateBranch(Project project, string branchName, BranchType branchType){ - var gitProject = await _gitlabClient.Projects.GetAsync(project.Gitlab.Id); + var gitlabClient = GetGitliabClient(project.Gitlab); + var gitProject = await gitlabClient.Projects.GetAsync(project.Gitlab.Id);+ private IGitLabClient GetGitliabClient(GitlabSettings repository) + { + if (repository?.HostUrl == null) + { + return GetGitlabClient(_gitlabOptions.Value); + } + + // 如果攜帶了 gitlab 設置, 則作為外部倉庫 + var gitlabOptions = new GitlabOptions() + { + HostUrl = repository.HostUrl, + AuthenticationToken = repository.AuthenticationToken + }; + return GetGitlabClient(gitlabOptions); + } + + private IGitLabClient GetGitlabClient(GitlabOptions gitlabOptions) + { + return new GitLabClient(gitlabOptions.HostUrl, gitlabOptions.AuthenticationToken); + } + }

      ? ? 對于以上實現,調用 ProjectService 會真實地調用 GitlabClient,注意這引入了依賴具體實現的反模式,代碼失去了可測試性。

      [Fact(Skip = "not implemented")] public async Task CreateBranch_UserExternalRepository_ShouldQuoteCommit() {var project = new Project{Gitlab = new GitlabSettings{Id = 1024,HostUrl = "https://gitee.com",AuthenticationToken = "token"}};// HOW? }

      ? ? 提問:如果需要取消 develop 分支的特殊性,允許用戶自行管理,在方法 GetBranch ReferenceForCreate()?上注釋掉分支判斷是否完成了需求?

      private String GetBranchReferenceForCreate(BranchType branchType) {return branchType switch{BranchType.Feature => Branch.Develop, -??????//?BranchType.Feature?=>?Branch.Develop,BranchType.Hotfix => Branch.Master,_ => throw new ArgumentOutOfRangeException(nameof(branchType), $"Not supported branchType {branchType}"),};

      ? ? 可以想象大片的測試用例會掛掉,因為該方法被廣泛使用并斷言。由于單元測試不再成功,單元測試對業務邏輯的保護也隨之消失。如果不修復單元測試,我們就無法保證其他業務不受影響。

      實戰小結

    • 良好的設計具有很好的可測試性,可測試性要求反過來會影響架構設計與領域實現;

    • 倉儲邏輯也能夠進行有效的測試;

    • 單元測試減少了回歸工作量,提升了交付質量。

    • 四、后話

      ? ? 以迭代緊張為理由在提交業務代碼時候忽略單元測試的編寫,是項目管理及開發人員對單元測試認識有限的體現。本文描述了定義和必要性,基于 DDD 架構進行了實踐,展示了單元測試如何影響業務邏輯甚至是架構設計。

    • 開發人員應認識和理解單元測試,熟練運用相關工具和技能;

    • 交付質量保證應在開發階段就由單元測試覆蓋率保證;

    • 測試先行體現了業務規則,要求邏輯自洽和場景覆蓋;

    • 可測試性要求會倒推架構合理性,避免架構劣化甚至反模式。

    • ------ END ------

      作者簡介

      馮同學:?研發工程師,目前負責開發云平臺相關研發工作。

      也許您還想看:

      技術分享|To B復雜系統的性能測試要注意哪些?

      ERP平臺的自動化測試技術實踐

      更多明源云·天際開放平臺場景案例與開發小知識,可以關注明源云天際開發者社區公眾號:

      建模零代碼之建模賬號接入DevOps 賬號體系

      繁星計劃·上海站 邁出企業數字化升級賦能第一步

      建模零代碼之業務組件的復用

    總結

    以上是生活随笔為你收集整理的技术分享|单元测试推广与实战-在全新的DDD架构上进行单元测试的全部內容,希望文章能夠幫你解決所遇到的問題。

    如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。