最美情侣中文字幕电影,在线麻豆精品传媒,在线网站高清黄,久久黄色视频

歡迎光臨散文網(wǎng) 會員登陸 & 注冊

領(lǐng)域驅(qū)動設(shè)計之單元測試最佳實踐(一)

2023-03-12 09:13 作者:吳小敏63  | 我要投稿

一直以來,我試圖找到一種有效的單元測試模式,使得“單元測試”真正能夠在團(tuán)隊中流行起來,讓單元測試不再是走過場,而是讓單元測試切切實實成為提高代碼質(zhì)量的途徑。

本文將描述一種以EF Code First模式實現(xiàn)的領(lǐng)域驅(qū)動項目實施單元測試的方案。

在描述這一方案之前,讓我們看看這一最佳實踐源于何種考慮和最終實現(xiàn)的目標(biāo):

1、以MVC項目為例,如果將單元測試的重心放在如何測試一個Controller或Action將收效甚微,原因有二:

  • 從原則上講Controller中不包含業(yè)務(wù)邏輯,理論上大部分代碼都是ViewModel和DTO之間的賦值或者Service的調(diào)用,對這樣的代碼編寫單元測試收效甚微,性價比極低。

  • Controller的代碼對UI的依賴度很高,也就意味著Controller的代碼不夠穩(wěn)定,這將迫使單元測試的變化頻率過高,容易給開發(fā)人員造成單元測試是一種負(fù)擔(dān)的心理。

基于這樣的原因,我將不建議人手緊張的團(tuán)隊對Controller編寫單元測試。

2、一個軟件項目真正需要測試的重心是業(yè)務(wù)邏輯,對一個領(lǐng)域驅(qū)動項目來說,領(lǐng)域邏輯才是重心。但是我們知道領(lǐng)域邏輯離不開數(shù)據(jù)的支撐,也就是說我們需要跟Repository打交道。

對于這樣的一個測試場景,大多數(shù)教程會提示你Mock Repository,從單元測試的角度來講,這樣的方案無疑是正確的,但是這樣的方案存在兩個問題:

  • 實際經(jīng)驗告訴我們這樣的測試不能真實的反應(yīng)出代碼的問題,甚至出現(xiàn)單元測試是通過的,可是Debug起來卻有問題。原因在于我們忽略了數(shù)據(jù)庫部分,這一部分邏輯處于失控狀態(tài)。

  • 需要Mock的數(shù)據(jù)太多,有時候為了測試一個邏輯,Mock的代碼比測試還要多,給開發(fā)人員造成單元測試其實就是在玩Mock的錯誤認(rèn)識。

所以我心目中理想的單元測試應(yīng)該具備以下條件:

  • 測試從Service->Repository->Domain一條線測試完畢,測試能夠準(zhǔn)確反應(yīng)出代碼是如何運行的。所以準(zhǔn)確來講我這個方案應(yīng)該叫“領(lǐng)域驅(qū)動設(shè)計之集成測試”。

  • 盡量不Mock,包括讀取數(shù)據(jù)庫部分。

  • 測試需要的數(shù)據(jù)應(yīng)該是可復(fù)用的,對測試“注冊用戶”、“搜索用戶”這樣的業(yè)務(wù)邏輯應(yīng)該能夠復(fù)用測試所提供的數(shù)據(jù)。

  • 任何測試都可以獨立運行,同一個測試多次執(zhí)行的效果應(yīng)該是一致的,測試的執(zhí)行速度盡可能快。

為了能夠盡可能的貼近這一目標(biāo),我實現(xiàn)了一個很簡單的DDD案例用來做測試用,這一案例描述了兩個重要的領(lǐng)域模型:User領(lǐng)域模型描述了“注冊用戶”、“更改密碼”、“登錄”等邏輯;BookManageProcess領(lǐng)域模型描述了“借書”、“歸還圖書”等邏輯,你可以理解為這是一個圖書館借書及還書的模型。

為了能夠理解此測試方案,我將對該測試案例做一個簡單描述:

該案例基于EF Code First和Castle實現(xiàn)的一個DDD案例,這一測試方案也是為DDD量身定制,并不適合于傳統(tǒng)的三層架構(gòu)。

為什么說這一案例是一個領(lǐng)域驅(qū)動案例?

以“用戶注冊”這一功能為例,我們來分析一下:

1、從UserService這一入口來看:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
????public?class?UserService : ApplicationService, IUserService
????{
????????private?readonly?IUserRepository _userRepository;
????????private?readonly?IEmailUniqueChecker _emailUniqueChecker;
?
????????public?UserService(IRepositoryContext context, IUserRepository userRepository,IEmailUniqueChecker emailUniqueChecker)
??????????: base(context)
????????{
????????????_userRepository = userRepository;
????????????_emailUniqueChecker = emailUniqueChecker;
????????}
?
????????public?Guid Register(UserModel userModel)
????????{
????????????var?user = User.Register(userModel,_emailUniqueChecker);
????????????_userRepository.Add(user);
????????????Context.Commit();
?
????????????return?user.Id;
????????}
}

Register()方法中幾乎只是對領(lǐng)域模型User.Register()方法的調(diào)用,其余的代碼都可以忽略不計,這說明了這樣一個事實:Service層沒有任何業(yè)務(wù)邏輯,所有的邏輯都應(yīng)該在Domain。

2、User領(lǐng)域模型中Register()方法的實現(xiàn):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
????public?partial?class?User
????{
????????public?static?User Register(UserModel userModel, IEmailUniqueChecker emailUniqueChecker)
????????{
????????????Contract.Requires(!userModel.Name.IsNullOrEmpty(), "invalid username");
?
????????????if?(emailUniqueChecker.IsExist(userModel.Email))
????????????{
????????????????throw?new?DuplicateEmailException("email already exist, please input another one");
????????????}
?
????????????var?password=new?Password(userModel.Password);
?
????????????var?user = new?User()
????????????{
????????????????Id = Guid.NewGuid(),
????????????????Name = userModel.Name,
????????????????Password = password.HashedPassword,
????????????????Salt = password.Salt,
????????????????Email = userModel.Email,
????????????????RegisterDateTime = DateTime.Now,
????????????????LastLoginDateTime = DateTime.Now
????????????};
?????????????
????????????return?user;
????????}
}

首先這是一個Patial類,因為另一部分描述屬性的內(nèi)容被EF用來操作數(shù)據(jù)庫。這一方法主要存在兩個邏輯:

對Email的檢查,以及對password的加密處理,正如你所見:這些邏輯反應(yīng)出了注冊一個用戶的實際邏輯是什么,而這些邏輯全部都應(yīng)該歸屬于Domain

由于在Domain中無法進(jìn)行依賴注入,所以我們從Service層通過方法傳入了IEmailUniqueChecker組件,具體實現(xiàn)如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public?class?EmailUniqueChecker:IEmailUniqueChecker
{
????private?readonly?IUserRepository _userRepository;
?
????public?EmailUniqueChecker(IUserRepository userRepository)
????{
????????_userRepository = userRepository;
????}
?
????public?bool?IsExist(string?email)
????{
????????var?user = _userRepository.Find(x => x.Email.ToLower() == email.ToLower()).FirstOrDefault();
?
????????return?user != null;
????}
}

而Password類測抽象了“密碼”的業(yè)務(wù)規(guī)則,同樣這一抽象應(yīng)該屬于Domain,讓我們來看看他的部分實現(xiàn):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
public?class?Password
????{
????????public?byte[] HashedPassword { get; private?set; }
????????public?byte[] Salt { get; }
?
????????public?Password(string?password)
????????{
????????????AssertPasswordMatchesPolicy(password);
?
????????????Salt = Guid.NewGuid().ToByteArray();
????????????HashedPassword = HashPassword(salt: Salt, password: password);
????????}
?
????????private?void?AssertPasswordMatchesPolicy(string?password)
????????{
????????????if?(password == null)
????????????{
????????????????var?error = Seq.Create("password can not be null");
?
????????????????throw?new?PasswordDoesNotMatchPolicyException(error);
????????????}
?
????????????var?errors = new?List<string>();
?
????????????if?(password.Trim().Length < 6)
????????????{
????????????????errors.Add("password shorter than six characters");
????????????}
????????????if?(password.ToLower() == password)
????????????{
????????????????errors.Add("password missing uppercase characters");
????????????}
????????????if?(password.ToUpper() == password)
????????????{
????????????????errors.Add("password missing lowercase characters");
????????????}
?
????????????if?(errors.Any())
????????????{
????????????????throw?new?PasswordDoesNotMatchPolicyException(errors);
????????????}
????????}
}

如果不是由于Password類的存在,所有這些代碼都應(yīng)該寫在User領(lǐng)域模型的Register()方法中。

繼續(xù)分析“用戶登錄”這一過程:

1、UserService中的入口:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public?bool?Login(string?email, string?password)
{
????var?user = _userRepository.Find(x => x.Email.ToLower() == email.ToLower()).FirstOrDefault();
????if?(user == null)
????{
????????throw??new?ApplicationServiceException("no such user");
????}
????if?(!user.Login(password))
????{
????????return?false;
????}
?
????_userRepository.Update(user);
????Context.Commit();
?
????return?true;
}

第一部分代碼我們可以認(rèn)為通過Email來獲取User領(lǐng)域模型,讀取到領(lǐng)域模型后調(diào)用user.Login()方法。這同樣說明了這樣一個事實:Service層沒有任何業(yè)務(wù)邏輯,所有的邏輯都應(yīng)該在Domain。

2、User領(lǐng)域模型中的Login實現(xiàn):

1
2
3
4
5
6
7
8
9
10
11
12
13
public?bool?Login(string?password)
{
????Contract.Requires(!password.IsNullOrEmpty(), "password can not be empty");
?
????var?hashedPassword = new?Password(Password, Salt);
????if?(hashedPassword.IsCorrectPassword(password))
????{
????????LastLoginDateTime = DateTime.Now;
????????return?true;
????}
?
????return?false;
}

正如你所見:這些邏輯反應(yīng)出了一個用戶登錄的實際邏輯是什么,而這些邏輯全部都應(yīng)該歸屬于Domain。


領(lǐng)域驅(qū)動設(shè)計之單元測試最佳實踐(一)的評論 (共 條)

分享到微博請遵守國家法律
古蔺县| 宜黄县| 满城县| 常德市| 天门市| 莱西市| 台南县| 荥经县| 东明县| 太仆寺旗| 松滋市| 招远市| 日照市| 库伦旗| 墨脱县| 潜山县| 浦县| 凌源市| 大渡口区| 东莞市| 肃宁县| 凭祥市| 江源县| 龙州县| 璧山县| 宜兰县| 广德县| 大庆市| 科尔| 沛县| 阿勒泰市| 平和县| 定襄县| 新竹县| 泸水县| 永新县| 巴里| 休宁县| 长乐市| 洛川县| 汽车|