![]() |
VOOZH | about |
一直以來,我試圖找到一種有效的單元測試模式,使得「單元測試」真正能夠在團隊中流行起來,讓單元測試不再是走過場,而是讓單元測試切切實實成為提高代碼質量的途徑。
本文將描述一種以EF Code First模式實現的領域驅動項目實施單元測試的方案。
在描述這一方案之前,讓我們看看這一最佳實踐源於何種考慮和最終實現的目標:
1、以MVC項目為例,如果將單元測試的重心放在如何測試一個Controller或Action將收效甚微,原因有二:
基於這樣的原因,我將不建議人手緊張的團隊對Controller編寫單元測試。
2、一個軟體項目真正需要測試的重心是業務邏輯,對一個領域驅動項目來說,領域邏輯才是重心。但是我們知道領域邏輯離不開數據的支撐,也就是說我們需要跟Repository打交道。
對於這樣的一個測試場景,大多數教程會提示你Mock Repository,從單元測試的角度來講,這樣的方案無疑是正確的,但是這樣的方案存在兩個問題:
所以我心目中理想的單元測試應該具備以下條件:
為了能夠儘可能的貼近這一目標,我實現了一個很簡單的DDD案例用來做測試用,這一案例描述了兩個重要的領域模型:User領域模型描述了「註冊用戶」、「更改密碼」、「登錄」等邏輯;BookManageProcess領域模型描述了「借書」、「歸還圖書」等邏輯,你可以理解為這是一個圖書館借書及還書的模型。
為了能夠理解此測試方案,我將對該測試案例做一個簡單描述:
該案例基於EF Code First和Castle實現的一個DDD案例,這一測試方案也是為DDD量身定製,並不適合於傳統的三層架構。
正如解決方案的截圖所示,這是一個非常簡單的案例,我給他起了一個還算霸氣的名字:MvcTests.BestPractice,至於為什麼叫MvcTests,是因為該測試方案可以用在Mvc+DDD的架構中,但是由於對Controller編寫測試的性價比極低,所以該方案中並為出現Controller的測試。
為什麼說這一案例是一個領域驅動案例?
以「用戶註冊」這一功能為例,我們來分析一下:
{private readonly IUserRepository _userRepository;private readonly IEmailUniqueChecker _emailUniqueChecker;UserService(IRepositoryContext context, IUserRepository userRepository,IEmailUniqueChecker emailUniqueChecker)(context)Guid Register(UserModel
userModel)user = User.Register(userModel,_emailUniqueChecker);
_userRepository.Add(user);Context.Commit;user.Id;
Register方法中幾乎只是對領域模型User.Register方法的調用,其餘的代碼都可以忽略不計,這說明了這樣一個事實:Service層沒有任何業務邏輯,所有的邏輯都應該在Domain。
User Register(UserModel userModel, IEmailUniqueChecker emailUniqueChecker)
{Contract.Requires(!userModel.Name.IsNullOrEmpty,"invalid username");(emailUniqueChecker.IsExist(userModel.Email)) DuplicateEmailException("email already exist, please input another one"); User
{Id = Guid.NewGuid,Name = userModel.Name,Password = password.HashedPassword,Salt = password.Salt,Email = userModel.Email,RegisterDateTime = DateTime.Now,LastLoginDateTime = DateTime.Now};user;
}首先這是一個Patial類,因為另一部分描述屬性的內容被EF用來操作資料庫。這一方法主要存在兩個邏輯:
對Email的檢查,以及對password的加密處理,正如你所見:這些邏輯反應出了註冊一個用戶的實際邏輯是什麼,而這些邏輯全部都應該歸屬於Domain。
由於在Domain中無法進行依賴注入,所以我們從Service層通過方法傳入了IEmailUniqueChecker組件,具體實現如下:
EmailUniqueChecker(IUserRepository userRepository)
{_userRepository = userRepository;}user = _userRepository.Find(x => x.Email.ToLower == email.ToLower).FirstOrDefault; user !=null;
}而Password類測抽象了「密碼」的業務規則,同樣這一抽象應該屬於Domain,讓我們來看看他的部分實現:
password)
{AssertPasswordMatchesPolicy(password);PasswordDoesNotMatchPolicyException(error); (password.Trim.Length < 6)
{errors.Add("password shorter than six characters");(password.ToLower == password)
{errors.Add("password missing uppercase characters");(password.ToUpper == password)
{errors.Add("password missing lowercase characters");PasswordDoesNotMatchPolicyException(errors);
}}}如果不是由於Password類的存在,所有這些代碼都應該寫在User領域模型的Register方法中。
繼續分析「用戶登錄」這一過程:
Login(string throw false;
第一部分代碼我們可以認為通過Email來獲取User領域模型,讀取到領域模型後調用user.Login方法。這同樣說明了這樣一個事實:Service層沒有任何業務邏輯,所有的邏輯都應該在Domain。
2、User領域模型中的Login實現:
Login(string password)(hashedPassword.IsCorrectPassword(password))