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/

  • 相关阅读:
    罗振宇 知识就是力量之怎样做一个不冲动的人
    C++中stl的map
    stl中顺序性容器,关联容器两者粗略解释
    stl之容器、迭代器、算法几者之间的关系
    mysql技术内幕之常规使用
    essential c++ 第一章 array及vector相关使用
    由函数clock想到的
    编程获得CPU的主频
    Markdown 基本语法
    Future FutrueTask Callable类源码说明以及原理使用
  • 原文地址:https://www.cnblogs.com/VolcanoCloud/p/4551976.html
Copyright © 2011-2022 走看看