介紹完了DDD案例,咱們終於能夠進入主題了,本方案的測試代碼基於Xunit編寫,斷言組件採用了FluentAssertions,相似的組件還有Shouldly。另外本案例使用了Code Contracts for .NET,若是不安裝此插件,可能有個別測試不能正確Pass。html
爲了實現目標中的第二點:"儘可能不Mock,包括數據庫讀取部分」,我嘗試過3種方案:前端
一、測試代碼鏈接真實數據庫,只須要將測試數據庫配置到測試項目中的web.config中,便可達到這一目標。可是該方案畢竟存在不少缺點,如:須要將測試庫和正式庫的更改保持同步,單元測試不利於集成在CI中,不利於團隊協做等。git
二、使用SQL Lite,可是因爲SQL lite自己不支持一些Linq表達式如:Skip,另外還有一些功能也沒法跟Sql server保持一致,最終放棄該方案。github
三、使用測試組件Effort,能夠很好的配合Entity framework使用,因爲Effort內部使用了關係型內存數據庫nmemory,因此很是適合運行單元測試。web
固然我仍是很是期待微軟可以編寫基於EF的單元測試組件。數據庫
我在《我眼中的領域驅動設計》一文中提到:不要使用數據庫獨有的技術,如存儲過程和觸發器等。一方面這些邏輯都應該是Domain邏輯,另外一方面一旦使用了這些技術也就意味着咱們沒法爲這些邏輯編寫測試。ide
1、使用Effort函數
爲了可以在Castle中使用基於Effort的DbContext,須要在Castle中註冊Effort:單元測試
public class FakeDbContextInstaller:IWindsorInstaller { public const string DbConnectionKey = "FakeDbConnection"; public const string FakeBookLibraryDbContextKey = "FakeBookLibraryDbContext"; public void Install(IWindsorContainer container, IConfigurationStore store) { container.Register( Component.For<DbConnection>().UsingFactoryMethod(DbConnectionFactory.CreateTransient) .Named(DbConnectionKey) .LifestylePerWebRequest() ); container.Register(Component.For<BookLibraryDbContext>() .DependsOn(Dependency.OnComponent(typeof(DbConnection), DbConnectionKey)) .Named(FakeBookLibraryDbContextKey) .LifestylePerWebRequest() .IsDefault()); } }
2、爲測試編寫場景測試
爲了複用測試數據,咱們須要編寫場景(Scenario),下面的文件組織結構描述了這一意圖:
以用戶註冊爲例,設計RegisterUserScenario:
public class RegisterUserScenario : ScenarioBase { public UserModel GivingModel { get; set; } public Guid Id { get; private set; } public RegisterUserScenario(IWindsorContainer container):base(container) { GivingModel = new UserModel() { Name = "Lilei", Password = "Password1", Email = "lilei@google.com", }; } public override void Execute() { var userService = Container.Resolve<IUserService>(); Id = userService.Register(GivingModel); } }
場景老是提供了正確的數據,執行這樣的場景老是可以獲得正確的結果:
[Fact] public void When_RegisterUserWithValidData_Should_CreateUser() { //Arrange var scenario=new RegisterUserScenario(Container); //Act scenario.Execute(); //Assert var user = UserService.GetUser(scenario.Id); user.Name.Should().Be(scenario.GivingModel.Name); user.Email.Should().Be(scenario.GivingModel.Email); }
測試的方法名很重要,咱們在讀完這個方法名以後就知道該測試是在幹嗎。
爲了獲得失敗的結果,咱們須要重寫Scenario中的數據,好比下面的測試:
[Fact] public void When_RegisterUserWithEmptyName_Should_ThrowException() { //Arrange var scenario=new RegisterUserScenario(Container) { GivingModel = new UserModel() { Name = string.Empty, Email = "lilei@google.com", Password = "Password1" } }; //Act scenario.Invoking(s => s.Execute()).ShouldThrow<Exception>("invalid username"); }
3、基於以前的場景編寫新的場景,從而達到複用數據的目的
例如咱們須要編寫「用戶登陸」的測試,首先須要編寫LoginScenario
public class LoginScenario:ScenarioBase { public string Email { get; set; } public string Password { get; set; } public bool Login { get; private set; } public Guid Id { get; private set; } public LoginScenario(IWindsorContainer container) : base(container) { var registerScenario=new RegisterUserScenario(container); registerScenario.Execute(); Id = registerScenario.Id; Email = registerScenario.GivingModel.Email; Password = registerScenario.GivingModel.Password; } public override void Execute() { var userService = Container.Resolve<IUserService>(); Login=userService.Login(Email, Password); } }
在這個場景的構造函數中咱們又執行了RegisterScenario,從而達到重複利用數據的目的。
爲「用戶登陸」編寫測試:
public class UserLoginTests:TestBase { [Fact] public void When_LoginWithInexistentEmail_Should_ThrowException() { //Arrange var loginScenario=new LoginScenario(Container) { Email = "other@google.com", }; //Act loginScenario.Invoking(s => s.Execute()).ShouldThrow<ApplicationServiceException>("no such user"); } [Fact] public void When_LoginWithWrongPassword_Should_ReturnFalse() { //Arrange var loginScenario=new LoginScenario(Container) { Password = "wrongPassword" }; //Act loginScenario.Execute(); //Assert loginScenario.Login.Should().BeFalse(); } [Fact] public void When_LoginWithCorrectPassword_Should_ReturnTrue() { //Arrange var loginScenario = new LoginScenario(Container); //Act loginScenario.Execute(); //Assert loginScenario.Login.Should().BeTrue(); } }
咱們老是須要爲新的業務邏輯編寫新的場景,而新的場景老是基於以前編寫好的場景,整個系統的任何功能均可以用真實的測試代碼來覆蓋。
因爲咱們在測試基類中爲每一個測試都開啓了單獨的scope,每個測試結束都會dispose數據庫。因此每個測試不管運行多少遍都是相同的效果。缺點是這些測試不能並行運行,XUnit默認以不一樣的測試類爲單位並行運行,咱們經過在測試類上添加相同的[Collection("IntegrationTests")]標籤,從而禁用XUnit的並行運行能力。
採用該方案覆蓋完畢單元測試的系統,開發者每次提交代碼並保證全部單元測是都是「passed」,開發者每一次代碼提交都會信心滿滿。
高質量的單元測試不但可以確保系統的平穩運行,更是一種有效的文檔,當你讀完每個場景的測試用例,你基本就可以對該業務很是熟悉了。
接近真實的單元測試還能夠省去你Debug的時間,只要你編寫的測試經過,基本就能夠確保後臺代碼的可靠性。另外你能夠在任什麼時候候從這些測試代碼中Debug進去,相比從前端界面Debug代碼可以節省很多時間,一勞永逸。
更多具體細節請查看源碼:https://git.oschina.net/richieyangs/BookLibrary.git