翻譯的初衷以及爲何選擇《Entity Framework 6 Recipes》來學習,請看本系列開篇html
問題數據庫
你想爲領域對象建立單元測試。app
這主要用於,測試特定的數據訪問功能。框架
解決方案ide
對於這個解決方案,使用POCO模板來建立你的實體。使用POC模板能減小你須要編寫的代碼量,還能讓你的解決方案很是清晰。固然,在解決方案中,你將運用手工建立的POCO類和下面的步驟。函數
假設你有如圖8-9所示的模型。工具
圖8-9. 一個包含reservation、schedule和train的模型單元測試
這個模型表示預訂火車出行。每一個預約都是一個特定的出行計劃。按下面的步驟建立模型和爲應用準備單元測試:學習
一、建立一個空的解決方案。右鍵解決方案,選擇Add(新增) ➤New Project(新建項目)。添加一個類庫項目。將它命名爲TrainReservation;測試
二、右鍵TrainReservation項目,選擇Add(新增) ➤New Item(新建項)。添加一個ADO.NET實體數據模型。導入表Train,Schedule和Reservation。最終的模型如圖8-9所示。
三、添加一個Ivalidate接口和ChangeAction枚舉,如代碼清單8-11所示。
代碼清單8-11. IValidate接口
public enum ChangeAction { Insert, Update, Delete } interface IValidate { void Validate(ChangeAction action); }
四、將代碼8-12中的代碼添加到項目中,它添加了類Reservation和Schedule的驗證代碼(實現接口IValidate)。
代碼清單8-12. 類Reservation和Schedule類實現IValidate接口
1 public partial class Reservation : IValidate 2 { 3 public void Validate(ChangeAction action) 4 { 5 if (action == ChangeAction.Insert) 6 { 7 if (Schedule.Reservations.Count(r => 8 r.ReservationId != ReservationId && 9 r.Passenger == this.Passenger) > 0) 10 throw new InvalidOperationException( 11 "Reservation for the passenger already exists"); 12 } 13 } 14 } 15 16 public partial class Schedule : IValidate 17 { 18 public void Validate(ChangeAction action) 19 { 20 if (action == ChangeAction.Insert) 21 { 22 if (ArrivalDate < DepartureDate) 23 { 24 throw new InvalidOperationException( 25 "Arrival date cannot be before departure date"); 26 } 27 28 if (LeavesFrom == ArrivesAt) 29 { 30 throw new InvalidOperationException( 31 "Can't leave from and arrive at the same location"); 32 } 33 } 34 } 35 }
五、使用代碼清單8-13中的代碼重寫DbContext中的SaveChanges()方法,這將容許你在保存數據到數據庫前驗證更改。
代碼清單8-13. 重寫SaveChages()方法
1 public override int SaveChanges() 2 { 3 this.ChangeTracker.DetectChanges(); 4 var entries = from e in this.ChangeTracker.Entries().Where(e => e.State == (System.Data.Entity.EntityState.Added | EntityState.Modified | EntityState.Deleted)) 5 where (e.Entity != null) && 6 (e.Entity is IValidate) 7 select e; 8 foreach (var entry in entries) 9 { 10 switch (entry.State) 11 { 12 case EntityState.Added: 13 ((IValidate)entry.Entity).Validate(ChangeAction.Insert); 14 break; 15 case EntityState.Modified: 16 ((IValidate)entry.Entity).Validate(ChangeAction.Update); 17 break; 18 case EntityState.Deleted: 19 ((IValidate)entry.Entity).Validate(ChangeAction.Delete); 20 break; 21 } 22 } 23 return base.SaveChanges(); 24 }
六、使用代碼清單8-14中的代碼建立IReservationContext接口,咱們將使用這個接口來幫助測試。它是一個虛假的上下文對象,它不會將更改真正地保存到數據庫。
代碼清單8-14. 使用接口IReservationContext來定義DbContext中須要的方法
public interface IReservationContext : IDisposable { IDbSet<Train> Trains { get; } IDbSet<Schedule> Schedules { get; } IDbSet<Reservation> Reservations { get; } int SaveChanges(); }
七、POCO模板生成了POCO類和實現了ObjectContext的上下文類。咱們須要這個上下文類實現IReservationContext接口。 爲了實現這個要求,咱們編輯Recipe8.Context.tt模板文件,在生成上下文對象名稱處添加IReservationContext。 這一行完整代碼以下:
<#=Accessibility.ForType(container)#> partial class <#=code.Escape(container)#> : DbContext,IReservationContext
八、使用代碼清單8-15建立倉儲類,這個類的構造函數接受一個IReservationContext類型的參數。
代碼清單8-15. 類ReservationRepository的構造函數接受一個IReservationContext類型的參數
1 public class ReservationRepository 2 { 3 private IReservationContext _context; 4 5 public ReservationRepository(IReservationContext context) 6 { 7 if (context == null) 8 throw new ArgumentNullException("context is null"); 9 _context = context; 10 } 11 public void AddTrain(Train train) 12 { 13 _context.Trains.Add(train); 14 } 15 16 public void AddSchedule(Schedule schedule) 17 { 18 _context.Schedules.Add(schedule); 19 } 20 21 public void AddReservation(Reservation reservation) 22 { 23 _context.Reservations.Add(reservation); 24 } 25 26 public void SaveChanges() 27 { 28 _context.SaveChanges(); 29 } 30 31 public List<Schedule> GetActiveSchedulesForTrain(int trainId) 32 { 33 var schedules = from r in _context.Schedules 34 where r.ArrivalDate.Date >= DateTime.Today && 35 r.TrainId == trainId 36 select r; 37 return schedules.ToList(); 38 } 39 }
九、右鍵解決方案,選擇Add(新增) ➤New Project(新建項目)。添加一個測試項目到解決方案。將這個項目命名爲Tests,並添加System.Data.Entity的引用。
十、使用代碼清單8-16,建立一個虛擬對象集和一個虛擬的DbContext,以方便你在沒有數據庫的狀況下隔離測試業務規則。
代碼清單8-16.實現虛擬對象集和虛擬的上下文對象
1 public class FakeDbSet<T> : IDbSet<T> 2 where T : class 3 { 4 HashSet<T> _data; 5 IQueryable _query; 6 7 public FakeDbSet() 8 { 9 _data = new HashSet<T>(); 10 _query = _data.AsQueryable(); 11 } 12 13 public virtual T Find(params object[] keyValues) 14 { 15 throw new NotImplementedException("Derive from FakeDbSet<T> and override Find"); 16 } 17 18 public T Add(T item) 19 { 20 _data.Add(item); 21 return item; 22 } 23 24 public T Remove(T item) 25 { 26 _data.Remove(item); 27 return item; 28 } 29 30 public T Attach(T item) 31 { 32 _data.Add(item); 33 return item; 34 } 35 36 public T Detach(T item) 37 { 38 _data.Remove(item); 39 return item; 40 } 41 42 public T Create() 43 { 44 return Activator.CreateInstance<T>(); 45 } 46 47 public TDerivedEntity Create<TDerivedEntity>() where TDerivedEntity : class, T 48 { 49 return Activator.CreateInstance<TDerivedEntity>(); 50 } 51 52 public System.Data.Entity.Infrastructure.DbLocalView<T> Local 53 { 54 get { return null; } 55 } 56 57 public System.Threading.Tasks.Task<T> FindAsync(System.Threading.CancellationToken token,params object[] keyValues) 58 { 59 throw new NotImplementedException("Derive from FakeDbSet<T> and override Find"); 60 } 61 62 Type IQueryable.ElementType 63 { 64 get { return _query.ElementType; } 65 } 66 System.Linq.Expressions.Expression IQueryable.Expression 67 { 68 get { return _query.Expression; } 69 } 70 71 IQueryProvider IQueryable.Provider 72 { 73 get { return _query.Provider; } 74 } 75 System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() 76 { 77 return _data.GetEnumerator(); 78 } 79 IEnumerator<T> IEnumerable<T>.GetEnumerator() 80 { 81 return _data.GetEnumerator(); 82 } 83 } 84 public class FakeReservationContext : IReservationContext, IDisposable 85 { 86 private IDbSet<Train> trains; 87 private IDbSet<Schedule> schedules; 88 private IDbSet<Reservation> reservations; 89 public FakeReservationContext() 90 { 91 trains = new FakeDbSet<Train>(); 92 schedules = new FakeDbSet<Schedule>(); 93 reservations = new FakeDbSet<Reservation>(); 94 } 95 96 public IDbSet<Train> Trains 97 { 98 get { return trains; } 99 } 100 101 public IDbSet<Schedule> Schedules 102 { 103 get { return schedules; } 104 } 105 106 public IDbSet<Reservation> Reservations 107 { 108 get { return reservations; } 109 } 110 111 public int SaveChanges() 112 { 113 foreach (var schedule in Schedules.Cast<IValidate>()) 114 { 115 schedule.Validate(ChangeAction.Insert); 116 } 117 foreach (var reservation in Reservations.Cast<IValidate>()) 118 { 119 reservation.Validate(ChangeAction.Insert); 120 } 121 return 1; 122 } 123 public void Dispose() 124 { 125 } 126 }
十一、咱們不想使用真正的數據庫來測試,因此咱們須要建立一個虛擬的DbContext,用它來模擬DbContext,它使用內存集合來扮演咱們的數據存儲。將代碼清單8-17中的單元測試代碼添加到項目中。
代碼清單8-18. 咱們測試項目中的代碼清單
1 [TestClass] 2 public class ReservationTest 3 { 4 private IReservationContext _context; 5 6 [TestInitialize] 7 public void TestSetup() 8 { 9 var train = new Train { TrainId = 1, TrainName = "Polar Express" }; 10 var schedule = new Schedule 11 { 12 ScheduleId = 1, 13 Train = train, 14 ArrivalDate = DateTime.Now, 15 DepartureDate = DateTime.Today, 16 LeavesFrom = "Dallas", 17 ArrivesAt = "New York" 18 }; 19 var reservation = new Reservation 20 { 21 ReservationId = 1, 22 Passenger = "Phil Marlowe", 23 Schedule = schedule 24 }; 25 _context = new FakeReservationContext(); 26 var repository = new ReservationRepository(_context); 27 repository.AddTrain(train); 28 repository.AddSchedule(schedule); 29 repository.AddReservation(reservation); 30 repository.SaveChanges(); 31 } 32 33 [TestMethod] 34 [ExpectedException(typeof(InvalidOperationException))] 35 public void TestForDuplicateReservation() 36 { 37 var repository = new ReservationRepository(_context); 38 var schedule = repository.GetActiveSchedulesForTrain(1).First(); 39 var reservation = new Reservation 40 { 41 ReservationId = 2, 42 Schedule = schedule, 43 Passenger = "Phil Marlowe" 44 }; 45 repository.AddReservation(reservation); 46 repository.SaveChanges(); 47 } 48 49 [TestMethod] 50 [ExpectedException(typeof(InvalidOperationException))] 51 public void TestForArrivalDateGreaterThanDepartureDate() 52 { 53 var repository = new ReservationRepository(_context); 54 var schedule = new Schedule 55 { 56 ScheduleId = 2, 57 TrainId = 1, 58 ArrivalDate = DateTime.Today, 59 DepartureDate = DateTime.Now, 60 ArrivesAt = "New York", 61 LeavesFrom = "Chicago" 62 }; 63 repository.AddSchedule(schedule); 64 repository.SaveChanges(); 65 } 66 67 [TestMethod] 68 [ExpectedException(typeof(InvalidOperationException))] 69 public void TestForArrivesAndLeavesFromSameLocation() 70 { 71 var repository = new ReservationRepository(_context); 72 var schedule = new Schedule 73 { 74 ScheduleId = 3, 75 TrainId = 1, 76 ArrivalDate = DateTime.Now, 77 DepartureDate = DateTime.Today, 78 ArrivesAt = "Dallas", 79 LeavesFrom = "Dallas" 80 }; 81 repository.AddSchedule(schedule); 82 repository.SaveChanges(); 83 } 84 } 85 }
測試項目有三個單元測試,它測試下面幾個業務規則:
一、一個乘客不能超過一個出行預約;
二、到達時間必須晚於出發時間;
三、出發地和目的地不能相同;
原理
咱們使用至關數量的代碼建立了一個完整的解決方案,它包含一個接口(IReservationContext),咱們用它來抽象對DbContext的引用,一個虛擬的DbSet(FakeDbSet<T>),一個虛擬的DbContext(FakeReservationContext),以及比較小的單元測試集。咱們使用虛擬的DbContext,是爲了避免與數據庫發生交互。測試的目的是,測試業務規則,而不是數據庫交互。
解決方案中的一個關鍵點是,咱們建立一個簡化的倉儲,用它來管理對象的插入和查詢。倉儲的構造函數接受一個IReservationContext類型的參數。爲了測試領域對象,咱們給它傳遞了一個FakeReservationContext的實例。若是容許將領域對象持久化到數據庫中,咱們須要傳遞一個真正的DBContext的實例:EFRecipesEntities。
咱們須要DbSets經過虛擬的DbContext,返回一個和真實上下文EFRecipesEntities返回相匹配的數據。爲了實現需求,咱們修改了T4模板,讓它生成的上下文返回IDbSet<T>來代替DbSet<T>。爲了確保虛擬的DbContext也返回IDbSet<T>類型的DbSet,咱們實現了本身的FakeDbset<T>,它派生至IDbSet<T>。
在測試項目中,我建立一個基於FakeReservationContext實例的ReservationRepository進行測試。單元測試與虛擬的FakReservationContext交互代替了與真實DbContext的交互。
最佳實踐
有兩個測試方法:定義一個倉儲接口,真正的倉儲和用於測試的一個或多個倉儲都須要實現它。 經過實現該接口,與持久化框架的交互能夠被隱藏在具體的實現中。不須要建立基礎設施其他部分的虛擬對象。它能簡化測試代碼的實現,但這可能會讓倉儲自身的代碼未被測試。
定義一個DbContext的接口,它公佈IDbSet<T>類型的屬性和SaveChanges()方法,正如本節所作的那樣。真正的DbContext和全部虛擬的DbContext必須實現這個接口。使用這種方法,你不須要虛擬整個倉儲,它可能會在某些狀況下不一樣。你的虛擬DbContext不須要模擬整個DbContext類的行爲;這可能會是一個挑戰。你須要在你的接口中限制你的代碼,夠用便可。
問題
你想使用數據庫測試你的倉儲。
這種方法常常被用來作集成測試,它測試完整的數據訪問功能。
解決方案
你建立了一個倉儲,管理全部的查詢、插入、更新和刪除。你想使用一個真正的數據庫實例來測試這個倉儲。假設你有如圖8-10的所示的模型。由於咱們測試時會建立和刪除數據庫,因此讓咱們從一個測試數據庫開始吧。
圖8-10. 一個關於書及目錄的模型
按下面的步驟測試倉儲:
一、建立一個空的解決方案。右鍵解決方案,選擇Add(新增) ➤New Project(新建項目)。添加一個類庫項目。將它命名爲BookRepository;
二、建立一個數據庫,命名爲Test。咱們會在單元測試中建立和刪除這個數據庫。因此你要確保從新建立一個空的數據庫;
三、添加表Book、Category及其關係到圖8-10所示的模型中。導入這些表到一個新的模型中,或者,你可使用Model First建立一個模型,而後用生成的數據庫腳原本建立數據庫;
四、添加代碼清單8-18中的代碼,建立一個BookRepository類,它經過模型處理插入和查詢;
代碼清單8-18. BookRepository類,經過模型處理插入和查詢;
1 public class BookRepository 2 { 3 private TestEntities _context; 4 5 public BookRepository(TestEntities context) 6 { 7 _context = context; 8 } 9 10 public void InsertBook(Book book) 11 { 12 _context.Books.Add(book); 13 } 14 15 public void InsertCategory(Category category) 16 { 17 _context.Categories.Add(category); 18 } 19 20 public void SaveChanges() 21 { 22 _context.SaveChanges(); 23 } 24 25 public IQueryable<Book> BooksByCategory(string name) 26 { 27 return _context.Books.Where(b => b.Category.Name == name); 28 } 29 30 public IQueryable<Book> BooksByYear(int year) 31 { 32 return _context.Books.Where(b => b.PublishDate.Year == year); 33 } 34 }
五、右鍵解決方案,選擇Add(新增) ➤New Project(新建項目)。添加一個測試項目。並添加System.Data.Entity和項目BookRepository的引用。
六、右鍵測試項目,選擇Add(新增) ➤New Test(新建測試)。添加一個單元測試到項目中。使用代碼清單8-19中的代碼建立測試類。
代碼清單8-19. 單元測試類BookRepositoryTest
1 [TestClass] 2 public class BookRepositoryTest 3 { 4 private TestEntities _context; 5 6 [TestInitialize] 7 public void TestSetup() 8 { 9 _context = new TestEntities(); 10 if (_context.Database.Exists()) 11 { 12 _context.Database.Delete(); 13 } 14 _context.Database.Create(); 15 } 16 17 [TestMethod] 18 public void TestsBooksInCategory() 19 { 20 var repository = new BookRepository.BookRepository(_context); 21 var construction = new Category { Name = "Construction" }; 22 var book = new Book 23 { 24 Title = "Building with Masonary", 25 Author = "Dick Kreh", 26 PublishDate = new DateTime(1998, 1, 1) 27 }; 28 book.Category = construction; 29 repository.InsertCategory(construction); 30 repository.InsertBook(book); 31 repository.SaveChanges(); 32 33 // test 34 var books = repository.BooksByCategory("Construction"); 35 Assert.AreEqual(books.Count(), 1); 36 } 37 38 [TestMethod] 39 public void TestBooksPublishedInTheYear() 40 { 41 var repository = new BookRepository.BookRepository(_context); 42 var construction = new Category { Name = "Construction" }; 43 var book = new Book 44 { 45 Title = "Building with Masonary", 46 Author = "Dick Kreh", 47 PublishDate = new DateTime(1998, 1, 1) 48 }; 49 book.Category = construction; 50 repository.InsertCategory(construction); 51 repository.InsertBook(book); 52 repository.SaveChanges(); 53 54 // test 55 var books = repository.BooksByYear(1998); 56 Assert.AreEqual(books.Count(), 1); 57 } 58 }
七、右鍵測試項目,選擇Add(新增) ➤New Item(新建項)。從General Templates(常規)中選擇應用程序配置文件。從BookRepository項目中的app.config文件中複製<connectionStrings>到測試項目的App.config文件中。
八、右鍵測試項目,選擇設置爲啓動項目。選擇Debug(調試) ➤Start Debugging(開始調試) 或者按F5執行測試。確保沒有數據庫鏈接連到測試數據庫。不然會致使DropDatabase()方法失敗。
原理
實體框架有兩種經常使用的測試方法。第一種是測試你的業務邏輯,對於這個方法,你會用一個」虛擬「的數據庫層,由於你的焦點是在業務邏輯上,這些邏管理着對象間的交互,以及保存到數據庫的規則。咱們在8-8節中演示了這種方法。
第二種方法是測試你的業務邏輯和數據持久化。這個方法用得比較普遍,同時也須要更多的時間和資源。當它實現自動測試工具時,像常常被用到持續集成環境中的測試工具,你須要自動建立和刪除測試數據庫。
每次迭代測試都須要一個新的數據庫狀態。後繼的測試不能被前面的測試數據影響。這種建立,刪除數據庫的端到端的測試,比起8-8中演示的邏輯測試須要更多的資源。
代碼清單8-19中的單元測試代碼,在測試初始化階段,咱們檢查了數據庫是否存在。若是存在,就使用DropDatabase()方法(譯註:代碼中使用的是Delete方法)將其刪除。而後使用CreateDatabase()方法(譯註:代碼中使用的是Create方法)建立新的數據庫。這些方法都使用配置文件App.config中的鏈接字符串。原本,這個鏈接字符串和開發庫中的鏈接字符串應該不同。爲了簡單起見,咱們對它們使用相同的鏈接字符串。
至此第八章結束。轉眼一個月就過去了,不知不覺中就更新了46篇了。回頭看,真是不容易。首先,感謝你們的閱讀,特別是爲我指出錯字別字,及翻譯上不當的朋友。其次,我得感謝個人老婆QTT和兒子FYH,由於,在這一個月的時間的,我差很少用了所有的業餘時間。若是沒有她的支持,確定不可能有這個系列的。同時,兒子才一歲多,正是須要爸爸陪着玩的時候,結果我成天抱着電腦,對此表示歉意。最後,感謝博客園爲咱們提供這樣一個學習的平臺。本系列的翻譯也不得不結束了。由於,聽朋友說,這種完整的翻譯會涉及版權問題。不管是出於對做者權益的維護,仍是自身權益的維護,我都不得不終止翻譯。不過,你們也不用擔憂,前八章已經基本介紹完了EF的知識點,後面是一些高級的,不多使用的知識。好比存儲過程、自定義上下文對象等。 若是對它們感興趣的話,只好煩麻你們閱讀原書了。歡迎你們一塊兒學習討論。後續,我將繼續介紹EF相關的知識,特別是EF7的知識和運用。感謝你們繼續關注!
實體框架交流QQ羣: 458326058,歡迎有興趣的朋友加入一塊兒交流
謝謝你們的持續關注,個人博客地址:http://www.cnblogs.com/VolcanoCloud/