65.9K
CodeProject 正在变化。 阅读更多。
Home

Moq - Mock 数据库

starIconstarIconstarIconstarIconstarIcon

5.00/5 (8投票s)

2012年10月18日

CPOL

4分钟阅读

viewsIcon

142642

使用 Moq 在单元测试中模拟数据库。

引言

Moq 是一个非常有用的框架,可以轻松地为您的单元测试模拟服务调用和方法。 

本文档将帮助您理解 Moq 如何用于模拟数据库(即为您的存储库项目编写单元测试用例)。 

这里我使用了 Microsoft Enterprise Library 对象(以便于理解),您可以非常方便地将其扩展到任何其他框架、实用程序或 ADO.NET 方法。我还会尝试介绍 Moq 中的一些高级概念,例如匿名方法、Callback()Queueing。   

背景

我使用 Moq 已经快一年了,发现很多人在模拟数据库时会遇到困难。我们中的许多人使用数据库的 Dev 实例,并让我们的测试用例调用实际的 SQL 实例。 

使用代码

首先,您的存储库应该有一个构造函数(或一个公共属性),您可以通过它从单元测试中传递模拟的数据库对象。

下面是这样的构造函数的示例:

public MyRepository(Databse Db)
{
  this.database = Db;
}

下面是“ExecuteScalar”方法的示例(它返回某个位置的员工数量)。

using (DbCommand cmd = database.GetStoredProcCommand(SPCREATEPRODUCTLIST))           
{ 
    this.database.AddParameter(cmd, "@Location", DbType.String, 0, 
      ParameterDirection.Input, true, 0, 0, "Location", DataRowVersion.Default, location);
    object result = database.ExecuteScalar(cmd);
}

您可以通过这种方式模拟一个标量方法。

private static Mock<Database> MockExecuteScalar(object returnValue)
{
   Mock<DbProviderFactory> mockedDBFactory = new Mock<DbProviderFactory>();
   Mock<Database> mockedDB = new Mock<Database>("MockedDB", mockedDBFactory.Object);
   mockedDB.Setup(x => x.ExecuteScalar(It.IsAny<DbCommand>())).Returns(returnValue);
   return mockedDB;
}

(您可以在 http://msdn.microsoft.com/en-us/library/ff648951.aspx 阅读更多关于 Enterprise library 及其实现的信息)。

这很简单,这个方法模拟了“ExecuteScalar”方法(由于该方法在 Database 类中被标记为 virtual,所以您可以模拟它。您可以轻松地模拟接口,而在模拟类时,只能模拟 virtual 属性和方法)。 

在您的单元测试用例中,您可以这样调用它:

Database mockedDB = MockExecuteScalar("5").Object;
MyRepository target = new MyRepository(mockedDB);
var result = target.GetEmployeeCount("London");

同样的方式,您可以模拟“ExecuteNonQuery”实现:

private static Mock<Database> MockExecuteNonQuery(object returnValue)
{
   Mock<DbProviderFactory> mockedDBFactory = new Mock<DbProviderFactory>();
   Mock<Database> mockedDB = new Mock<Database>("MockedDB", mockedDBFactory.Object);
   mockedDB.Setup(x => x.ExecuteNonQuery(It.IsAny<DbCommand>())).Returns(1);         
   return mockedDB;
}

现在,让我们转向“ExecuteReader”实现。ExecuteReader 可以是一个行集合,我们循环遍历 DataReader 流直到数据结束。所以这里有两个函数需要模拟。

  1. ExecuteReader() - 用于获取实际数据
  2. Read() - 返回 true 直到我们获得所需数据

以下是使用“ExecuteReader”的典型实现的示例:

using (DbCommand cmd = database.GetStoredProcCommand("GetEmployeeDetails", parameters))
{
    using (IDataReader dr = database.ExecuteReader(cmd))
    {
        while (dr.Read())
        {
           listofEmployeeDetails.Add(new Employee
           {
                EmployeeId = dr["EmpID"].ToString();
                EmployeeName = dr["EmployeeName"].toString();
                Location = dr["Location"].toString(); 
           });           
        }
    }
}

首先,让我们看一个简单的例子,在这个例子中,我们将模拟“ExecuteReader”以从我们的 MockedDatabase 返回单行数据。

步骤 1:模拟“Read”方法

在模拟 read 方法之前,我想简要介绍一下 Moq 函数中的匿名方法和 Callback() 方法。

Callback()

我们已经看到了 .Returns() 方法,它会为被模拟的函数调用返回响应。如果您想在控制权从 Return() 返回后执行自定义逻辑,可以使用 Callback()。  

这看起来会像这样:

mockedObject.Setup(x=>x.myMethod(It.IsAny<string>())).Returns("Hello").Callback(//custom logic goes here);

匿名方法

当您多次调用被模拟的方法并希望动态更改返回值时,匿名方法会非常方便。

以下是一个示例:

string returnValue = "Hello"  
mockedObject.Setup(x=>x.myMethod(It.IsAny<string>())).Returns(()=>returnValue).Callback(()=>returnValue="World");

当我们第一次调用“myMethod”时,返回值将是“Hello”,从第二次开始,它将返回“World”。您可以根据需要在此匿名方法中放置任何条件或自定义实现。

现在,在此场景中,我们希望“ExecuteReader”方法读取一行数据。那么在这种情况下,dataReader.Read() 方法应该只在第一次返回 true。

所以,我们可以这样模拟 .Read() 方法:

var mockedDataReader = new Mock<IDataReader>();
bool readFlag = true;
mockedDataReader.Setup(x => x.Read()).Returns(() => readFlag).Callback(() => readFlag = false);

步骤 2:模拟 ExecuteReader

在我们模拟“ExecuteReader”方法之前,我们需要设置响应数据。所以当我调用 dr["EmpID"] 时 

我将获得我想要的模拟值。我们可以通过以下方式实现:
mockedDataReader.Setup(x => x["EmpID"]).Returns("43527");  
mockedDataReader.Setup(x => x["EmployeeName"]).Returns("Smith");  
mockedDataReader.Setup(x => x["Location"]).Returns("London"); 
现在我们将模拟“ExecuteReader”方法,它将返回我们的模拟对象。
  Mock<DbProviderFactory> mockedDBFactory = new Mock<DbProviderFactory>();
  Mock<Database> mockedDB = new Mock<Database>("MockedDB", mockedDBFactory.Object);
  mockedDB.Setup(x => x.ExecuteReader(It.IsAny<DbCommand>())).Returns(mockedDataReader.Object); 
上面的方式与“ExecuteScalar”和“ExecuteNonQuery”相同,但在这里我们返回的是自定义的 DataReader 对象。下面是完整的函数样子:
private static Mock<Database> MockExecuteReader(Dictionary<string, object> returnValues)
{
    var mockedDataReader = new Mock<IDataReader>();
    bool readFlag = true;
    mockedDataReader.Setup(x => x.Read()).Returns(() => readFlag).Callback(() => readFlag = false);
    foreach (KeyValuePair<string, object> keyVal in returnValues)
    {
        mockedDataReader.Setup(x => x[keyVal.Key]).Returns(keyVal.Value);
    }          
    Mock<DbProviderFactory> mockedDBFactory = new Mock<DbProviderFactory>();
    Mock<Database> mockedDB = new Mock<Database>("MockedDB", mockedDBFactory.Object);
    mockedDB.Setup(x => x.ExecuteReader(It.IsAny<DbCommand>())).Returns(mockedDataReader.Object);
    return mockedDB;
} 

有时您可能需要从数据库中选择多行

在我开始解释如何模拟多行之前,让我解释一下 Return() 函数中的一个棘手问题。

假设我模拟了一个方法,并在我的代码中调用了它多次。

mockedService.Setup(x=>x.myMethod(It.IsAny<string>())).Returns("First");
mockedService.Setup(x=>x.myMethod(It.IsAny<string>())).Returns("Second"); 
mockedService.Setup(x=>x.myMethod(It.IsAny<string>())).Returns("Third"); 
上面的代码乍一看可能没问题。但每次都会给出“Third”的输出。

这里匿名函数非常方便,但我们需要确保输出的顺序正确。我们可以通过使用 Queue 来实现。代码会像这样:

Queue<object> responseQueue = new Queue<object>();
responseQueue.Enqueue("First");
responseQueue.Enqueue("Second");
responseQueue.Enqueue("Third");
mockedService.Setup(x=>x.myMethod(It.IsAny<string>())).Returns(()=>responseQueue.Dequeue()); 
如果您注意到,Returns() 方法现在将调用一个匿名方法,该方法将逐个出队(dequeue)值。 

为了返回多行,我们需要类似的东西,其中我们需要逐个 Dequeue() 每一行。完整的函数如下: 

private static Mock<Database> MockExecuteReader(List<Dictionary<string, object>> returnValues)
{
  var mockedDataReader = new Mock<IDataReader>();
  int count = 0;
  Queue<object> responseQueue = new Queue<object>();
  mockedDataReader.Setup(x => x.Read()).Returns(() => count<returnValues.Count).Callback(() => count++);
  returnValues.ForEach(rows =>
  {
   foreach (KeyValuePair<string, object> keyVal in rows)
   {
       responseQueue.Enqueue(keyVal.Value);
       mockedDataReader.Setup(x => x[keyVal.Key]).Returns(()=>responseQueue.Dequeue());
   }
  });
  Mock<DbProviderFactory> mockedDBFactory = new Mock<DbProviderFactory>();
  Mock<Database> mockedDB = new Mock<Database>("MockedDB", mockedDBFactory.Object);
  mockedDB.Setup(x => x.ExecuteReader(It.IsAny<DbCommand>())).Returns(mockedDataReader.Object);
  return mockedDB;
}

如果您注意到 Read() 的模拟是基于您想要返回的模拟数据行数量(List<> 的长度)来确定的。

每次 Callback() 调用时,本地变量 count 都会增加,这样当它超过数据行数时,Read() 方法就会返回 false。

您可以在所有单元测试中实现匿名方法、Callback 方法和 Queuing 的技术。在模拟存储库时,您可以使用这些通用方法来模拟您的数据库。

下载 Moq - (http://code.google.com/p/moq/)    

模拟愉快 微笑 | :)
© . All rights reserved.