数据访问层的单元测试

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了数据访问层的单元测试相关的知识,希望对你有一定的参考价值。

出处:http://www.cnblogs.com/wintersun/ 

数据访问层在分层结构,比较常见. 有时可能是数据访问模块. 假设数据访问层后端是数据库,那我们如何测试他们的呢? 有时实际这种测试是集成测试了.
有时数据库里还有一些逻辑,触发器,约束等. 个人十分不建议把业务逻辑放在数据库里实现. 最常见的数据库表的操作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框架来实现对数据库的隔离, 如下代码演示了我们使用Moqv4.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对象来实现隔离数据库的访问

以上是关于数据访问层的单元测试的主要内容,如果未能解决你的问题,请参考以下文章

集成测试框架

java spring项目的controller层的代码怎么用junit写单元测试用例

OA项目CRUD和单元测试

分层测试_基本思想

如何对 WCF 服务进行单元测试?

service层的单元测试(springboot)