《Entity Framework 6 Recipes》中文翻譯系列 (46) ------ 第八章 POCO之領域對象測試和倉儲測試

翻譯的初衷以及爲何選擇《Entity Framework 6 Recipes》來學習,請看本系列開篇html

8-8  測試領域對象

問題數據庫

  你想爲領域對象建立單元測試。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-9  使用數據庫測試倉儲

問題

  你想使用數據庫測試你的倉儲。

  這種方法常常被用來作集成測試,它測試完整的數據訪問功能。

解決方案

  你建立了一個倉儲,管理全部的查詢、插入、更新和刪除。你想使用一個真正的數據庫實例來測試這個倉儲。假設你有如圖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/

相關文章
相關標籤/搜索