前言
祝大家國慶快樂,本來想國慶之前更新完的,結果沒寫完,今天把剩下的程式碼補了一下總算ok了。
本章節也是我們後端日常開發中最重要的一步就是測試,我們經常聽到的單元測試、整合測試、UI測試、系統測試,還有就是最常見的(人肉測試),這些理論知識我記得老張有個視訊講了2篇,歡迎大家可以去群裡騷擾老張,我找不到那個連結了。
話不多說直接上車!
理論速過
ABP 框架的設計考慮了可測試性。有一些不同級別的自動化測試;
- 單元測試:您通常測試單個類(或幾個類一起)。這些測試會很快。但是,您通常需要處理服務依賴項的模擬。
- 整合測試:您通常會測試服務,但這次您不會模擬基本的基礎設施和服務,以檢視它們是否可以正常協同工作。
- UI 測試:您測試應用程式的 UI,就像使用者與您的應用程式互動一樣。###
單元測試與整合測試
與單元測試相比,整合測試有一些顯著的優勢;
- 更容易編寫,因為您不需要建立模擬和處理依賴項。
- 測試程式碼與所有真實的服務和基礎設施(包括資料庫對映和查詢)一起執行,因此它更接近於真實的應用程式測試。
雖然它們有一些缺點;
- 與單元測試相比,它們更慢,因為所有的基礎設施都是為每個測試用例準備的。
- 服務中的一個錯誤可能會導致多個測試用例被破壞,因此在某些情況下可能更難找到真正的問題。
我們建議混合使用:在必要的地方編寫單元或整合測試,並且您認為編寫和維護它是有效的。
Abp的預設已經幫我們生成了測試模板
- Domain.Tests用於測試您的域層物件(如域服務和實體)。
- Application.Tests用於測試您的應用程式層(如Application Services)。
- EntityFrameworkCore.Tests用於測試您的自定義儲存庫實現或 EF Core 對映(如果您使用其他資料庫提供程式,此專案將有所不同)。
- TestBase 包含一些被其他專案共享/使用的類。
注意:HttpApi.Client.ConsoleTestApp不是自動化測試應用程式。這是一個示例控制檯應用程式,展示瞭如何從 .NET 控制檯應用程式使用您的 HTTP API。
單元測試
簡單來看一下如何完成一個單元測試,依Blog
實體為例,實體是領域層的一部分,我們應該在Domain.Tests專案中對其進行測試。Blog_Tests在Domain.Tests專案中建立一個類(領域服務同理、)
此測試遵循 AAA(安排-行為-斷言)模式;
- 安排部分建立一個Blog實體。
- 行為部分執行我們要為此案例測試的方法。
- 斷言部分檢查Blog屬性是否與我們期望的相同。
public class Blog_Tests
{
[Theory]
[InlineData("aaa")]
[InlineData("bbb")]
public void SetName(string name)
{
var blog = new Blog(Guid.NewGuid(), "test blog", "test");
blog.SetName(name);
blog.Name.ShouldBe(name);
}
[Theory]
[InlineData("aaa")]
[InlineData("bbb")]
public void SetShortName(string name)
{
var blog = new Blog(Guid.NewGuid(), "test blog", "test");
blog.SetShortName(name);
blog.ShortName.ShouldBe(name);
}
}
整合測試
ABP 提供了一個完整的基礎設施來編寫整合測試。所有 ABP 基礎設施和服務都將在您的測試中執行。應用程式啟動模板附帶為您預先配置的必要基礎設施,啟動模板配置為對 EF Core使用記憶體中的 SQLite資料庫(對於 MongoDB,它使用Mongo2Go庫)。因此,所有配置和查詢都是針對真實資料庫執行的,您甚至可以測試資料庫事務。
種子資料
針對空資料庫編寫測試是不切實際的。大多數情況下,需要在資料庫中獲取一些初始資料。
在Bcvp.Blog.Core.TestBase
層的CoreTestDataSeedContributor.cs
中建立種子資料,這樣我們就可以使用這些資料來測試了。
public class CoreTestData : ISingletonDependency
{
public Guid Blog1Id { get; } = Guid.NewGuid();
public Guid Blog1Post1Id { get; } = Guid.NewGuid();
public Guid Blog1Post2Id { get; } = Guid.NewGuid();
public Guid Blog1Post1Comment1Id { get; } = Guid.NewGuid();
public Guid Blog1Post1Comment2Id { get; } = Guid.NewGuid();
public string Tag1Name { get; } = "Tag1Name";
public string Tag2Name { get; } = "Tag2Name";
}
public class CoreTestDataSeedContributor : IDataSeedContributor, ITransientDependency
{
private readonly CoreTestData _testData;
private readonly IBlogRepository _blogRepository;
private readonly IPostRepository _postRepository;
private readonly ICommentRepository _commentRepository;
private readonly ITagRepository _tagRepository;
private readonly ICurrentTenant _currentTenant;
public CoreTestDataSeedContributor(
CoreTestData testData,
IBlogRepository blogRepository,
IPostRepository postRepository,
ICommentRepository commentRepository,
ITagRepository tagRepository,
ICurrentTenant currentTenant)
{
_testData = testData;
_blogRepository = blogRepository;
_postRepository = postRepository;
_commentRepository = commentRepository;
_tagRepository = tagRepository;
_currentTenant = currentTenant;
}
public async Task SeedAsync(DataSeedContext context)
{
/* Seed additional test data... */
using (_currentTenant.Change(context?.TenantId))
{
await SeedBlogsAsync();
await SeedPostsAsync();
await SeedCommentsAsync();
await SeedTagsAsync();
}
}
private async Task SeedBlogsAsync()
{
await _blogRepository.InsertAsync(new BlogCore.Blogs.Blog(_testData.Blog1Id, "The First Blog", "blog-1"));
}
private async Task SeedPostsAsync()
{
await _postRepository.InsertAsync(new Post(_testData.Blog1Post1Id, _testData.Blog1Id, "title", "coverImage", "url"));
await _postRepository.InsertAsync(new Post(_testData.Blog1Post2Id, _testData.Blog1Id, "title2", "coverImage2", "url2"));
}
public async Task SeedCommentsAsync()
{
await _commentRepository.InsertAsync(new Comment(_testData.Blog1Post1Comment1Id, _testData.Blog1Post1Id, null, "text"));
await _commentRepository.InsertAsync(new Comment(_testData.Blog1Post1Comment2Id, _testData.Blog1Post1Id, _testData.Blog1Post1Comment1Id, "text"));
}
public async Task SeedTagsAsync()
{
await _tagRepository.InsertAsync(new Tag(Guid.NewGuid(), _testData.Blog1Id, _testData.Tag1Name, 10));
await _tagRepository.InsertAsync(new Tag(Guid.NewGuid(), _testData.Blog1Id, _testData.Tag2Name));
}
}
小插曲
開頭我說中間出了點問題,這裡我說一下,首先是我有2個介面名字寫錯了,Resharper應該自動幫我加Async字尾,但是有2個介面沒有很奇怪。
另一個問題之前我們開發的時候在使用者操作上我們呼叫的UserLookupService<IdentityUsers>
,這個用法是錯誤的,UserLookupService
是基於IUser的一個擴充套件操作類public abstract class UserLookupService<TUser, TUserRepository>
,我們應該整合IUser然後繼承它來寫,程式碼我也補充上了,如果你是從第一章開始跟這個小插曲正好可以讓你重新複習一遍開發流程也挺好的,但是如果不是單元測試我還真沒發現這個坑(我也是第一次用UserLookupService 之前都是直接呼叫IdentityUserManager),所以單元測試還是非常重要的。
完結撒花
最後撒花,整個基礎篇系列到此應該就結束了,從框架介紹到功能開發到測試,我們全方位對ABP進行了一次使用教學,雖然很多知識點沒有用到,但是整篇文章操作下來,入門肯定是ok了。
後面我會開始對ABP獨立的知識點進行講解其中會涉及原始碼應用場景等多種案例,目前的安排是模組化->授權->eventbus->考慮中 如果你對那塊知識點感興趣可以聯絡我,大家一起邊寫邊學。
聯絡作者:加群:867095512 @MrChuJiu
專案原始碼地址:https://github.com/BaseCoreVueProject/ABPvNext.Blog.Core