posts - 558,  comments - 551,  trackbacks - 20
公告

     数据访问层在分层结构,比较常见. 有时可能是数据访问模块. 假设数据访问层后端是数据库,那我们如何测试他们的呢? 有时实际这种测试是集成测试了.
有时数据库里还有一些逻辑,触发器,约束等. 个人十分不建议把业务逻辑放在数据库里实现. 最常见的数据库表的操作create, read, update和delete(简称CRUD)
, 例如我们需要测试某个Add方法,在这个测试方法完成后, 希望这条测试数据清除掉. 这样做是 为了不影响其它的测试方法也访问同一数据库对象.
    首先,我们可以使用.net 2.0中提供的TransactionScope类, 下面的代码基于MsTest的单元测试:

   1:      [TestClass]
   2:      public class DbTestBase
   3:      {
   4:          private TransactionScope scope;
   5:   
   6:          [TestInitialize]
   7:          public void SetUp()
   8:          {
   9:              this.scope = new TransactionScope();
  10:          }
  11:          
  12:          [TestCleanup]
  13:          public void TearDown()
  14:          {
  15:              this.scope.Dispose();
  16:          }
  17:      }

上面代码我们看到在标记TestInitialize特性SetUp方法中创建TransactionScope的实例,在TestCleanup特性TearDown方法中调用TransactionScope的Dispose方法.  然后我们继承这个测试基类:

   1:      [TestClass]
   2:      public class DateBaseTesting : DbTestBase
   3:      {
   4:          /// <summary>
   5:          /// Test Insert record to database
   6:          /// </summary>
   7:          /// <seealso cref="http://www.cnblogs.com/wintersun"/>
   8:          /// <remarks>Any database modification will be roll back</remarks>
   9:          [TestMethod]
  10:          public void TestAddWithEmployeeRepository()
  11:          {
  12:              //arrange
  13:              var employee = this.CreateNewEmployee();
  14:              var employRepository = RepositoryHelper.GetEmployeeRepository();
  15:   
  16:              //act
  17:              employRepository.Add(employee);
  18:              employRepository.Save();
  19:   
  20:              //assert
  21:              var employeelist =
  22:                 employRepository.Repository.Find(e => e.EmployeeID == employee.EmployeeID);
  23:              Assert.IsNotNull(employeelist);
  24:              CollectionAssert.AreEqual(new List<Employee>() { employee }, employeelist.ToList());
  25:          }
  26:   
  27:   
  28:          private Employee CreateNewEmployee()
  29:          {
  30:              var employee = new Employee
  31:              {
  32:                  ManagerID = 2,
  33:                  ContactID = 3,
  34:                  Title = "Developer",
  35:                  BirthDate = new DateTime(1965, 1, 1, 0, 0, 0),
  36:                  HireDate = DateTime.Now,
  37:                  Gender = "M",
  38:                  MaritalStatus = "M",
  39:                  ModifiedDate = DateTime.Now,
  40:                  NationalIDNumber = "2",
  41:                  rowguid = new Guid(),
  42:                  CurrentFlag = true,
  43:                  VacationHours = 2,
  44:                  SickLeaveHours = 3,
  45:                  SalariedFlag = false,
  46:                  LoginID = "myworkbase\\peter"
  47:              };
  48:              return employee;
  49:          }
  50:   
  51:      }

上面的TestAddWithEmployeeRepository中场景是数据访问层基于EntityFramework的Repository模式, 这里的操作是先是创建实体,然后是提交.  实际中可以是您的任何代码块,ADO.NET或其他的数据访问组件. 当我们执行这个单元测试后,这个TransactionScope将被释放. 之前插入的那条记录将被清除.
      假设你不想用基类, 只是简单在某个方法中, 可以这样做:

   1:          [TestMethod]
   2:          public void TestWrapTransactionScope()
   3:          {
   4:              WrapTransactionScope(() => TestAddWithEmployeeRepository());
   5:          }
   6:   
   7:          /// <summary>
   8:          /// Wraps the transaction scope for unit testing
   9:          /// </summary>
  10:          /// <param name="action">The action method</param>
  11:          /// <remarks>author http://www.cnblogs.com/wintersun </remarks>
  12:          public void WrapTransactionScope(Action action)
  13:          {
  14:              using (var scope = new TransactionScope())
  15:              {
  16:                  action();
  17:              }
  18:          }

      上面的代码演示了, 我们用Action委托实现另一个方法包装一下目标方法使其在TransactionScope块中.
      我们还可以使用xUnit 类库 这实现同样的操作, 这里使用是xUnit 1.8版本. 还有MbUnit,NUnit的[RollBack]特性与这个类似. 如果您还不熟悉怎么使用xUnit请先看这里介绍. 下面的代码使用 xUnit 代码变得更加简洁:

   1:      /// <summary>
   2:      /// Database unit testing with xUnit demo
   3:      /// </summary>
   4:      /// <remarks>http://wintersun.cnblogs.com </remarks>
   5:      public class TestDbWithxUnit
   6:      {
   7:          [Fact]
   8:          [AutoRollback]
   9:          public void Test_Add_One_Enity()
  10:          {
  11:              //arrange
  12:              var employee = this.CreateNewEmployee();
  13:              var employRepository = RepositoryHelper.GetEmployeeRepository();
  14:   
  15:              //act
  16:              employRepository.Add(employee);
  17:              employRepository.Save();
  18:   
  19:              //assert
  20:              var employeelist =
  21:                 employRepository.Repository.Find(e => e.EmployeeID == employee.EmployeeID);
  22:              Assert.NotNull(employeelist);
  23:              Assert.Equal(new List<Employee> { employee }, employeelist.ToList());
  24:          }
  25:  }

上面的代码我们只需在方法上加一个AutoRollback的Attribute就可以了, 注意你先需要引用xunit.extensions.dll
当然,最标准的单元测试我们完全隔离数据库, 数据访问组件在单元测试中不应该访问数据库. 我们使用Mock框架来实现对数据库的隔离, 如下代码演示了我们使用Moq v4.0.20926 和 xUnit 来实现的单元测试:

   1:          [Fact]
   2:          public void TestWithMoq()
   3:          {
   4:              //arrange
   5:              var mock = new Mock<IRepository<Employee>>();
   6:              Employee employee = this.CreateNewEmployee();
   7:              var list = new List<Employee>() { employee };
   8:   
   9:              mock.Setup(ep => ep.Add(It.IsAny<Employee>()))
  10:                  .Callback(()=>Console.WriteLine("Add()"));
  11:              mock.Setup(ep => ep.Find(It.IsAny<Expression<Func<Employee, bool>>>()))
  12:                  .Returns(list);
  13:              mock.Setup(ep => ep.Save())
  14:               .Callback(() => Console.WriteLine("Save()"));
  15:              var employeeRespository = mock.Object;
  16:             
  17:              //act
  18:              employeeRespository.Add(employee);
  19:              employeeRespository.Save();
  20:   
  21:              //verify method 
  22:              mock.Verify(ep => ep.Add(It.IsAny<Employee>()));
  23:              mock.Verify(ep => ep.Save());
  24:   
  25:              //assert
  26:              var employeelist =
  27:                employeeRespository.Find(e => e.EmployeeID == employee.EmployeeID);
  28:              Assert.NotNull(employeelist);
  29:              Assert.Equal(new List<Employee> { employee }, employeelist.ToList());
  30:          }

Employee是实体类, IRepository<T>是一个统一数据访问interface, 实现martinfowler的Repository模式;  上面的代码Mock了其中的Add,Save方法, 在调用后并做了Verify,确认之前Mock的方法有被调用到,  最后的部分才是Assert语句块. 代码很简单.

除了使用Mock框架, 我们还可以使用依赖注入容器来注入一个具体的Fake对象实现对访问数据库的隔离, 我们使用Unity 2.1 来实现:

   1:          [Fact]
   2:          public void TestWithDIContainer()
   3:          {
   4:              //arrange
   5:              var employee = this.CreateNewEmployee();
   6:              var list = new List<Employee>() { employee };
   7:              var employeeRespository = GetRepositoryInstance("for testing");
   8:   
   9:              //act
  10:              employeeRespository.Add(employee);
  11:              employeeRespository.Save();
  12:   
  13:              //assert
  14:              var employeelist =
  15:                employeeRespository.Find(e => e.EmployeeID == employee.EmployeeID);
  16:              Assert.NotNull(employeelist);
  17:              Assert.Equal(new List<Employee> { employee }, employeelist.ToList());
  18:          }
  19:   
  20:          private IRepository<Employee> GetRepositoryInstance(string name)
  21:          {
  22:              //Initial container
  23:              var container = new UnityContainer();
  24:              container.RegisterType<IRepository<Employee>, EFRepository<Employee>>();
  25:              container.RegisterType<IRepository<Employee>, FakeRepository<Employee>>("for testing");
  26:              //....
  27:              return container.Resolve<IRepository<Employee>>(name);
  28:          }

上面的代码为了演示的简便, 我们实现创建容器注册对象在一个private方法中; 您也可以使用其它IOC/DI的容器. FakeRepository<Employee>是一个实现了IRepository<T>的具体类, 在其内部可以在内存中操作实体,或是其它方式. 具体看您的需求了.

以上所有是我们介绍对数据访问层或数据库进行单元测试的解决方法:

1. 使用TransactionScope类实现测试方法回滚
2. 使用xUnit类库的AutoRollback特性实现自动回滚
3. 使用Moq框架来实现隔离数据库的访问
4. 使用Unity容器注入Fake对象来实现隔离数据库的访问
  
希望这篇文章对您做开发与单元测试有帮助.
您可以感兴趣的文章:

使用Moq来Mock多个接口
MsTest中实现类似NUnit中Assert.Throws
Repository Factory介绍
Unity在Xml配制文件使用泛型


作者:Petter Liu
出处:http://www.cnblogs.com/wintersun/
本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。
该文章也同时发布在我的独立博客中-Petter Liu Blog

posted on 2011-11-19 23:05 PetterLiu 阅读(...) 评论(...) 编辑 收藏