Moq - Mock 数据库





5.00/5 (8投票s)
使用 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
流直到数据结束。所以这里有两个函数需要模拟。
- ExecuteReader() - 用于获取实际数据
- 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/)
模拟愉快