我正在尝试定义一种方法来模拟访问数据库而不访问的情况......这可能听起来很疯狂,但事实并非如此。
以下是我想测试的方法示例:
public IDevice GetDeviceFromRepository(string name)
{
IDevice device = null;
IDbConnection connection = new SqlConnection(ConnectionString);
connection.Open();
try
{
IDbCommand command = connection.CreateCommand();
command.CommandText = string.Format("SELECT DEVICE_ID,DEVICE_NAME FROM DEVICE WHERE DEVICE_NAME='{0}'", name);
IDataReader dataReader = command.ExecuteReader();
if(dataReader.NextResult())
{
device = new Device(dataReader.GetInt32(0),dataReader.GetString(1));
}
}
finally
{
connection.Close();
}
return device;
}
我假装模仿IDataReader,所以我可以控制正在阅读的内容。类似的东西(使用Moq框架):
[TestMethod()]
public void GetDeviceFromRepositoryTest()
{
Mock<IDataReader> dataReaderMock = new Mock<IDataReader>();
dataReaderMock.Setup(x => x.NextResult()).Returns(true);
dataReaderMock.Setup(x => x.GetInt32(0)).Returns(000);
dataReaderMock.Setup(x => x.GetString(1)).Returns("myName");
Mock<IDbCommand> commandMock = new Mock<IDbCommand>();
commandMock.Setup(x => x.ExecuteReader()).Returns(dataReaderMock.Object);
Mock<RemoveDeviceManager> removeMock = new Mock<RemoveDeviceManager>();
removeMock.Setup()
RemoveDeviceManager target =new RemoveDeviceManager(new Device(000, "myName"));
string name = string.Empty;
IDevice expected = new Device(000, "myName"); // TODO: Initialize to an appropriate value
IDevice actual;
actual = target.GetDeviceFromRepository(name);
Assert.AreEqual(expected.SerialNumber, actual.SerialNumber);
Assert.AreEqual(expected.Name, actual.Name);
}
我的问题是我是否可以通过模拟方法强制方法GetDeviceFromRepository替换IDataReader。
答案 0 :(得分:5)
我认为这里的问题是你最终直接依赖 SqlConnection 。如果你使用一些依赖注入的变体,这样你的代码就可以访问IDbCommand而不知道它是如何构造的,你就可以毫不费力地注入你的模拟。
我理解这并不能完全回答你的问题,但从长远来看,所做的事情会给你带来更好的可测试性。
答案 1 :(得分:5)
我同意弗兰克的回答,即转向依赖注入是更好的长期解决方案,但是你可以采取一些中间步骤让你朝这个方向前进,而不会把整个事情搞砸。
有一件事是将IDbConnection类的构造移动到类中受保护的虚方法中:
protected virtual IDbConnection CreateConnection()
{
return new SqlConnection(ConnectionString);
}
然后,您可以像这样创建一个类的测试版本:
public class TestingRemoteDeviceManager : RemoteDeviceManager
{
public override IDbConnection CreateConnection()
{
IDbConnection conn = new Mock<IDbConnection>();
//mock out the rest of the interface, as well as the IDbCommand and
//IDataReader interfaces
return conn;
}
}
返回Mock或伪IDbConnection而不是具体的SqlConnection。那假货可以返回一个假的IDbCommand对象,然后可以返回一个假的IDataReader对象。
答案 2 :(得分:5)
咒语是test until fear is transformed in boredom。我想你已经越过这条线了。如果您将控制数据读取器,那么您正在测试的唯一代码是:
device = new Device(dataReader.GetInt32(0),dataReader.GetString(1));
这里几乎没有什么可测试的,这很好:数据层应该简单而愚蠢。因此,不要尝试对数据层进行单元测试。如果您认为必须对其进行测试,那么请将其与真实数据库进行集成测试。
当然,将数据层隐藏在IDeviceRepository
界面后面以便您可以轻松地模拟它以测试其他代码仍然是一个好主意。
答案 3 :(得分:5)
虽然您目前正在使用Moq,但我认为除非您使用Typemock Isolator(免责声明 - 我在Typemock 工作),否则无需依赖注入就无法获得您正在寻找的功能。
Isolator有一个名为“future objects”的功能,可以用以前创建的假对象替换对象的未来实例化:
// Create fake (stub/mock whateever) objects
var fakeSqlConnection = Isolate.Fake.Instance<SqlConnection>();
var fakeCommand = Isolate.Fake.Instance<SqlCommand>();
Isolate.WhenCalled(() => fakeSqlConnection.CreateCommand()).WillReturn(fakeCommand);
var fakeReader = Isolate.Fake.Instance<SqlDataReader>();
Isolate.WhenCalled(() => fakeCommand.ExecuteReader()).WillReturn(fakeReader);
// Next time SQLConnection is instantiated replace with our fake
Isolate.Swap.NextInstance<SqlConnection>().With(fakeSqlConnection);