翻译的初衷以及为什么选择《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接口

     public partial class Reservation : IValidate
{
public void Validate(ChangeAction action)
{
if (action == ChangeAction.Insert)
{
if (Schedule.Reservations.Count(r =>
r.ReservationId != ReservationId &&
r.Passenger == this.Passenger) > )
throw new InvalidOperationException(
"Reservation for the passenger already exists");
}
}
} public partial class Schedule : IValidate
{
public void Validate(ChangeAction action)
{
if (action == ChangeAction.Insert)
{
if (ArrivalDate < DepartureDate)
{
throw new InvalidOperationException(
"Arrival date cannot be before departure date");
} if (LeavesFrom == ArrivesAt)
{
throw new InvalidOperationException(
"Can't leave from and arrive at the same location");
}
}
}
}

    5、使用代码清单8-13中的代码重写DbContext中的SaveChanges()方法,这将允许你在保存数据到数据库前验证更改。

代码清单8-13. 重写SaveChages()方法

     public override int SaveChanges()
{
this.ChangeTracker.DetectChanges();
var entries = from e in this.ChangeTracker.Entries().Where(e => e.State == (System.Data.Entity.EntityState.Added | EntityState.Modified | EntityState.Deleted))
where (e.Entity != null) &&
(e.Entity is IValidate)
select e;
foreach (var entry in entries)
{
switch (entry.State)
{
case EntityState.Added:
((IValidate)entry.Entity).Validate(ChangeAction.Insert);
break;
case EntityState.Modified:
((IValidate)entry.Entity).Validate(ChangeAction.Update);
break;
case EntityState.Deleted:
((IValidate)entry.Entity).Validate(ChangeAction.Delete);
break;
}
}
return base.SaveChanges();
}

    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类型的参数

     public class ReservationRepository
{
private IReservationContext _context; public ReservationRepository(IReservationContext context)
{
if (context == null)
throw new ArgumentNullException("context is null");
_context = context;
}
public void AddTrain(Train train)
{
_context.Trains.Add(train);
} public void AddSchedule(Schedule schedule)
{
_context.Schedules.Add(schedule);
} public void AddReservation(Reservation reservation)
{
_context.Reservations.Add(reservation);
} public void SaveChanges()
{
_context.SaveChanges();
} public List<Schedule> GetActiveSchedulesForTrain(int trainId)
{
var schedules = from r in _context.Schedules
where r.ArrivalDate.Date >= DateTime.Today &&
r.TrainId == trainId
select r;
return schedules.ToList();
}
}

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

    10、使用代码清单8-16,创建一个虚拟对象集和一个虚拟的DbContext,以方便你在没有数据库的情况下隔离测试业务规则。

代码清单8-16.实现虚拟对象集和虚拟的上下文对象

     public class FakeDbSet<T> : IDbSet<T>
where T : class
{
HashSet<T> _data;
IQueryable _query; public FakeDbSet()
{
_data = new HashSet<T>();
_query = _data.AsQueryable();
} public virtual T Find(params object[] keyValues)
{
throw new NotImplementedException("Derive from FakeDbSet<T> and override Find");
} public T Add(T item)
{
_data.Add(item);
return item;
} public T Remove(T item)
{
_data.Remove(item);
return item;
} public T Attach(T item)
{
_data.Add(item);
return item;
} public T Detach(T item)
{
_data.Remove(item);
return item;
} public T Create()
{
return Activator.CreateInstance<T>();
} public TDerivedEntity Create<TDerivedEntity>() where TDerivedEntity : class, T
{
return Activator.CreateInstance<TDerivedEntity>();
} public System.Data.Entity.Infrastructure.DbLocalView<T> Local
{
get { return null; }
} public System.Threading.Tasks.Task<T> FindAsync(System.Threading.CancellationToken token,params object[] keyValues)
{
throw new NotImplementedException("Derive from FakeDbSet<T> and override Find");
} Type IQueryable.ElementType
{
get { return _query.ElementType; }
}
System.Linq.Expressions.Expression IQueryable.Expression
{
get { return _query.Expression; }
} IQueryProvider IQueryable.Provider
{
get { return _query.Provider; }
}
System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
{
return _data.GetEnumerator();
}
IEnumerator<T> IEnumerable<T>.GetEnumerator()
{
return _data.GetEnumerator();
}
}
public class FakeReservationContext : IReservationContext, IDisposable
{
private IDbSet<Train> trains;
private IDbSet<Schedule> schedules;
private IDbSet<Reservation> reservations;
public FakeReservationContext()
{
trains = new FakeDbSet<Train>();
schedules = new FakeDbSet<Schedule>();
reservations = new FakeDbSet<Reservation>();
} public IDbSet<Train> Trains
{
get { return trains; }
} public IDbSet<Schedule> Schedules
{
get { return schedules; }
} public IDbSet<Reservation> Reservations
{
get { return reservations; }
} public int SaveChanges()
{
foreach (var schedule in Schedules.Cast<IValidate>())
{
schedule.Validate(ChangeAction.Insert);
}
foreach (var reservation in Reservations.Cast<IValidate>())
{
reservation.Validate(ChangeAction.Insert);
}
return ;
}
public void Dispose()
{
}
}

    11、我们不想使用真正的数据库来测试,所以我们需要创建一个虚拟的DbContext,用它来模拟DbContext,它使用内存集合来扮演我们的数据存储。将代码清单8-17中的单元测试代码添加到项目中。

代码清单8-18. 我们测试项目中的代码清单

 [TestClass]
public class ReservationTest
{
private IReservationContext _context; [TestInitialize]
public void TestSetup()
{
var train = new Train { TrainId = , TrainName = "Polar Express" };
var schedule = new Schedule
{
ScheduleId = ,
Train = train,
ArrivalDate = DateTime.Now,
DepartureDate = DateTime.Today,
LeavesFrom = "Dallas",
ArrivesAt = "New York"
};
var reservation = new Reservation
{
ReservationId = ,
Passenger = "Phil Marlowe",
Schedule = schedule
};
_context = new FakeReservationContext();
var repository = new ReservationRepository(_context);
repository.AddTrain(train);
repository.AddSchedule(schedule);
repository.AddReservation(reservation);
repository.SaveChanges();
} [TestMethod]
[ExpectedException(typeof(InvalidOperationException))]
public void TestForDuplicateReservation()
{
var repository = new ReservationRepository(_context);
var schedule = repository.GetActiveSchedulesForTrain().First();
var reservation = new Reservation
{
ReservationId = ,
Schedule = schedule,
Passenger = "Phil Marlowe"
};
repository.AddReservation(reservation);
repository.SaveChanges();
} [TestMethod]
[ExpectedException(typeof(InvalidOperationException))]
public void TestForArrivalDateGreaterThanDepartureDate()
{
var repository = new ReservationRepository(_context);
var schedule = new Schedule
{
ScheduleId = ,
TrainId = ,
ArrivalDate = DateTime.Today,
DepartureDate = DateTime.Now,
ArrivesAt = "New York",
LeavesFrom = "Chicago"
};
repository.AddSchedule(schedule);
repository.SaveChanges();
} [TestMethod]
[ExpectedException(typeof(InvalidOperationException))]
public void TestForArrivesAndLeavesFromSameLocation()
{
var repository = new ReservationRepository(_context);
var schedule = new Schedule
{
ScheduleId = ,
TrainId = ,
ArrivalDate = DateTime.Now,
DepartureDate = DateTime.Today,
ArrivesAt = "Dallas",
LeavesFrom = "Dallas"
};
repository.AddSchedule(schedule);
repository.SaveChanges();
}
}
}

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

    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类,通过模型处理插入和查询;

 public class BookRepository
{
private TestEntities _context; public BookRepository(TestEntities context)
{
_context = context;
} public void InsertBook(Book book)
{
_context.Books.Add(book);
} public void InsertCategory(Category category)
{
_context.Categories.Add(category);
} public void SaveChanges()
{
_context.SaveChanges();
} public IQueryable<Book> BooksByCategory(string name)
{
return _context.Books.Where(b => b.Category.Name == name);
} public IQueryable<Book> BooksByYear(int year)
{
return _context.Books.Where(b => b.PublishDate.Year == year);
}
}

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

    6、右键测试项目,选择Add(新增) ➤New Test(新建测试)。添加一个单元测试到项目中。使用代码清单8-19中的代码创建测试类。

代码清单8-19. 单元测试类BookRepositoryTest

 [TestClass]
public class BookRepositoryTest
{
private TestEntities _context; [TestInitialize]
public void TestSetup()
{
_context = new TestEntities();
if (_context.Database.Exists())
{
_context.Database.Delete();
}
_context.Database.Create();
} [TestMethod]
public void TestsBooksInCategory()
{
var repository = new BookRepository.BookRepository(_context);
var construction = new Category { Name = "Construction" };
var book = new Book
{
Title = "Building with Masonary",
Author = "Dick Kreh",
PublishDate = new DateTime(, , )
};
book.Category = construction;
repository.InsertCategory(construction);
repository.InsertBook(book);
repository.SaveChanges(); // test
var books = repository.BooksByCategory("Construction");
Assert.AreEqual(books.Count(), );
} [TestMethod]
public void TestBooksPublishedInTheYear()
{
var repository = new BookRepository.BookRepository(_context);
var construction = new Category { Name = "Construction" };
var book = new Book
{
Title = "Building with Masonary",
Author = "Dick Kreh",
PublishDate = new DateTime(, , )
};
book.Category = construction;
repository.InsertCategory(construction);
repository.InsertBook(book);
repository.SaveChanges(); // test
var books = repository.BooksByYear();
Assert.AreEqual(books.Count(), );
}
}

    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/

《Entity Framework 6 Recipes》中文翻译系列 (46) ------ 第八章 POCO之领域对象测试和仓储测试的更多相关文章

  1. 《Entity Framework 6 Recipes》中文翻译系列 (42) ------ 第八章 POCO之使用POCO

    翻译的初衷以及为什么选择<Entity Framework 6 Recipes>来学习,请看本系列开篇 第八章 POCO 对象不应该知道如何保存它们,加载它们或者过滤它们.这是软件开发中熟 ...

  2. 《Entity Framework 6 Recipes》中文翻译系列 (43) ------ 第八章 POCO之使用POCO加载实体

    翻译的初衷以及为什么选择<Entity Framework 6 Recipes>来学习,请看本系列开篇 8-2  使用POCO加载关联实体 问题 你想使用POCO预先加载关联实体. 解决方 ...

  3. 《Entity Framework 6 Recipes》中文翻译系列 (44) ------ 第八章 POCO之POCO中使用值对象和对象变更通知

    翻译的初衷以及为什么选择<Entity Framework 6 Recipes>来学习,请看本系列开篇 8-4  POCO中使用值对象(Complex Type--也叫复合类型)属性 问题 ...

  4. 《Entity Framework 6 Recipes》中文翻译系列 (45) ------ 第八章 POCO之获取原始对象与手工同步对象图和变化跟踪器

    翻译的初衷以及为什么选择<Entity Framework 6 Recipes>来学习,请看本系列开篇 8-6  获取原始对象 问题 你正在使用POCO,想从数据库获取原始对象. 解决方案 ...

  5. 《Entity Framework 6 Recipes》翻译系列 (1) -----第一章 开始使用实体框架之历史和框架简述

    微软的Entity Framework 受到越来越多人的关注和使用,Entity Framework7.0版本也即将发行.虽然已经开源,可遗憾的是,国内没有关于它的书籍,更不用说好书了,可能是因为EF ...

  6. 《Entity Framework 6 Recipes》翻译系列(2) -----第一章 开始使用实体框架之使用介绍

    Visual Studio 我们在Windows平台上开发应用程序使用的工具主要是Visual Studio.这个集成开发环境已经演化了很多年,从一个简单的C++编辑器和编译器到一个高度集成.支持软件 ...

  7. 《Entity Framework 6 Recipes》翻译系列 (4) -----第二章 实体数据建模基础之从已存在的数据库创建模型

    不知道对EF感兴趣的并不多,还是我翻译有问题(如果是,恳请你指正),通过前几篇的反馈,阅读这个系列的人不多.不要这事到最后成了吃不讨好的事就麻烦了,废话就到这里,直奔主题. 2-2 从已存在的数据库创 ...

  8. 《Entity Framework 6 Recipes》翻译系列 (3) -----第二章 实体数据建模基础之创建一个简单的模型

    第二章 实体数据建模基础 很有可能,你才开始探索实体框架,你可能会问“我们怎么开始?”,如果你真是这样的话,那么本章就是一个很好的开始.如果不是,你已经建模,并在实体分裂和继承方面感觉良好,那么你可以 ...

  9. 《Entity Framework 6 Recipes》翻译系列 (5) -----第二章 实体数据建模基础之有载荷和无载荷的多对多关系建模

    2-3 无载荷(with NO Payload)的多对多关系建模 问题 在数据库中,存在通过一张链接表来关联两张表的情况.链接表仅包含连接两张表形成多对多关系的外键,你需要把这两张多对多关系的表导入到 ...

随机推荐

  1. Idea 开发 web项目

    1.经历 很久没有搞 web 项目了,最近一段时间搞过很多次了,但是总是在 mac 上部署失败. 2.方法: 用idea 新建一个模板的 Spring MVC 项目,部署就可以了. 3.参考: htt ...

  2. python之实现基于paramiko和mysql数据库的堡垒机

    一.堡垒机结构 堡垒机执行流程: 管理员为用户在服务器上创建账号(将公钥放置服务器,或者使用用户名密码) 用户登陆堡垒机,输入堡垒机用户名密码,现实当前用户管理的服务器列表 用户选择服务器,并自动登陆 ...

  3. (十四)UDP协议的两个主要方法sendto和recvfrom详解

    在网络编程中,UDP运用非常广泛.很多网络协议是基于UDP来实现的,如SNMP等.大家常常用到的局域网文件传输软件飞鸽传书也是基于UDP实现的. 本篇文章跟大家分享linux下UDP的使用和实现,主要 ...

  4. 图解集合5:不正确地使用HashMap引发死循环及元素丢失

    问题引出 前一篇文章讲解了HashMap的实现原理,讲到了HashMap不是线程安全的.那么HashMap在多线程环境下又会有什么问题呢? 几个月前,公司项目的一个模块在线上运行的时候出现了死循环,死 ...

  5. 【Git 】$ ./gradlew idea 构建一个idea的项目

    Welcome to Git (version 1.9.5-preview20150319) Run 'git help git' to display the help index.Run 'git ...

  6. 无法捕获的异常:MissingMethodException

    今天一个同事发布站点,一直出现一些稀奇古怪的问题,各种各样的异常都有,根据这些异常去排查代码,都完全正常,很让人郁闷,因为代码里可能出异常的地方都记录了程序日志,所以他一直没去排查系统里的“应用程序日 ...

  7. 用Mockito测试SpringMVC+Hibernate

    用Mockito测试SpringMVC+Hibernate 译自:Spring 4 MVC+Hibernate 4+MySQL+Maven integration + Testing example ...

  8. linux安装navicat全程记录

    国庆期间自己在试着用linux(ubuntu),献上navicat安装方法,以及很多教程里没有写的一些小东西 step1: 去navicat官网下载安装包,网址:http://www.navicat. ...

  9. 五、docker网络技术

    五 docker网络技术 1.本章环境: 源码文件目录: 2.网络基础回顾 通道: NAT将私有地址和端口号翻译成公有的地址和端口号项某网站发出数据包.某网站根据数据表查出私有ip和端口号返回数据. ...

  10. oracle删除表字段和oracle表增加字段

    这篇文章主要介绍了oracle表增加字段.删除表字段修改表字段的使用方法,大家参考使用吧   添加字段的语法:alter table tablename add (column datatype [d ...