zoukankan      html  css  js  c++  java
  • 《Entity Framework 6 Recipes》中文翻译系列 (46) ------ 第八章 POCO之领域对象测试和仓储测试

    翻译的初衷以及为什么选择《Entity Framework 6 Recipes》来学习,请看本系列开篇

    8-8  测试领域对象

    问题

      你想为领域对象创建单元测试。

      这主要用于,测试特定的数据访问功能。

    解决方案

      对于这个解决方案,使用POCO模板来创建你的实体。使用POC模板能减少你需要编写的代码量,还能让你的解决方案非常清晰。当然,在解决方案中,你将运用手工创建的POCO类和下面的步骤。

      假设你有如图8-9所示的模型。

    图8-9. 一个包含reservation、schedule和train的模型

      这个模型表示预订火车出行。每个预定都是一个特定的出行计划。按下面的步骤创建模型和为应用准备单元测试:

        1、创建一个空的解决方案。右键解决方案,选择Add(新增) ➤New Project(新建项目)。添加一个类库项目。将它命名为TrainReservation;

        2、右键TrainReservation项目,选择Add(新增) ➤New Item(新建项)。添加一个ADO.NET实体数据模型。导入表Train,Schedule和Reservation。最终的模型如图8-9所示。

        3、添加一个Ivalidate接口和ChangeAction枚举,如代码清单8-11所示。

    代码清单8-11. IValidate接口

    public enum ChangeAction
    {
        Insert,
        Update,
        Delete
    }
    interface IValidate
    {
        void Validate(ChangeAction action);
    }

        4、将代码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     }

        5、使用代码清单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         }

        6、使用代码清单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();
        }

        7、POCO模板生成了POCO类和实现了ObjectContext的上下文类。我们需要这个上下文类实现IReservationContext接口。 为了实现这个要求,我们编辑Recipe8.Context.tt模板文件,在生成上下文对象名称处添加IReservationContext。 这一行完整代码如下:

    <#=Accessibility.ForType(container)#> partial class <#=code.Escape(container)#> :
    DbContext,IReservationContext

        8、使用代码清单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     }

        9、右键解决方案,选择Add(新增) ➤New Project(新建项目)。添加一个测试项目到解决方案。将这个项目命名为Tests,并添加System.Data.Entity的引用。

        10、使用代码清单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     }

        11、我们不想使用真正的数据库来测试,所以我们需要创建一个虚拟的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 }

      测试项目有三个单元测试,它测试下面几个业务规则:

        1、一个乘客不能超过一个出行预定;

        2、到达时间必须晚于出发时间;

        3、出发地和目的地不能相同;

    原理

       我们使用相当数量的代码创建了一个完整的解决方案,它包含一个接口(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. 一个关于书及目录的模型

       按下面的步骤测试仓储:

        1、创建一个空的解决方案。右键解决方案,选择Add(新增) ➤New Project(新建项目)。添加一个类库项目。将它命名为BookRepository;

        2、创建一个数据库,命名为Test。我们会在单元测试中创建和删除这个数据库。所以你要确保重新创建一个空的数据库;

        3、添加表Book、Category及其关系到图8-10所示的模型中。导入这些表到一个新的模型中,或者,你可以使用Model First创建一个模型,然后用生成的数据库脚本来创建数据库;

        4、添加代码清单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     }

        5、右键解决方案,选择Add(新增) ➤New Project(新建项目)。添加一个测试项目。并添加System.Data.Entity和项目BookRepository的引用。

        6、右键测试项目,选择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     }

        7、右键测试项目,选择Add(新增) ➤New Item(新建项)。从General Templates(常规)中选择应用程序配置文件。从BookRepository项目中的app.config文件中复制<connectionStrings>到测试项目的App.config文件中。

        8、右键测试项目,选择设置为启动项目。选择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/

  • 相关阅读:
    TextBox 只有下划线
    can't find web control library(web控件库)
    DropDownListSalesAC”有一个无效 SelectedValue,因为它不在项目列表中。
    IDE、SATA、SCSI、SAS、FC、SSD 硬盘类型
    如何打印1px表格
    CSS控制打印 分页
    Virtual Server could not open its emulated Ethernet switch driver. To fix this problem, reenable the Virtual Server Emulated Et
    Xml中SelectSingleNode方法中的xpath用法
    热带水果莫入冰箱?水果存放冰箱大法
    探索Asp.net的Postback机制
  • 原文地址:https://www.cnblogs.com/VolcanoCloud/p/4551976.html
Copyright © 2011-2022 走看看