目录
介绍
在本文中,我们将解释如何创建一个对单元测试友好的数据库访问类,并且使用普通的ADO.NET类完成,而无需更复杂的框架。测试将使用XUnit和Moq实现。这些示例使用C#和NET 5实现,但也可以在其他版本的NET中实现,例如NET Core 3.1
背景
传统上,使用ADO.NET的开发人员通过在其上直接实现用于管理数据库访问的对象来创建Data类,通常我们使用连接对象的具体实现(例如,SqlConnection)来实现数据访问类。
这种形式不允许创建依赖于接口存在的类的模拟。该接口允许我们创建一个假对象来实现模拟。
我发现很多开发人员认为不可能对DB类做一个mock,因为ADO.NET类的具体实现中缺少接口(例如SQLCommand, 或SQLConnection),事实是存在一个通用接口允许我们这样做。
IDbConnection Interface (System.Data) | Microsoft Learn
IDbConnection允许我们使用它在类中注入它,而不是连接的具体实现,或者在代码中使用new创建它。
在我们的代码中,因为实际上在数据库访问类的所有实例中注入相同的对象可能会产生一些并发问题,所以我们使用委托将函数传递给db类,而不是直接从IDbConnection派生的对象的实例。这确保了在我们的类实例化中使用的对象对于该类来说是唯一的,从而避免了并发问题。
将类实现为可单元测试
我们如何实现它,以及在实际程序中使用访问数据库我们需要遵循三个简单的步骤。
第一步
在startup.cs类中配置要注入对象的函数。
public void ConfigureServices(IServiceCollection services)
{
// Rest of code .....
string connectionStr = Configuration.GetConnectionString("Wheater");
services.AddScoped<IMoqReadyService, MoqReadyService>(
x => new MoqReadyService(() => new SqlConnection(connectionStr)));
}
在这段代码中观察到,我们从配置中获取连接字符串,工厂函数被编码为在调用时创建一个新SqlConnection对象。
第二步
创建数据访问类并将函数作为参数注入构造函数中。
/// <summary>
/// Factory for IDb Connection
/// </summary>
private Func<IDbConnection> Factory { get; }
/// <summary>
/// Class Constructor
/// </summary>
/// <param name="factory">The IdbConnection compatible factory function</param>
public MoqReadyService(Func<IDbConnection> factory)
{
this.Factory = factory;
}
如您所见,我们在构造函数中将函数注入到类中并将其存储在private变量中。
第三步
调用工厂并创建其余所需的对象。
最后一步,调用由内到外的工厂方法来创建我们的实例SqlConnection(如本例中所配置)并创建其余的ADO.NET对象:
public async Task<List<WeatherForecast>> GetForecastMoqableAsync(DateTime startDate)
{
var t = await Task.Run(() =>
{
// This invoke the factory and create the SqlCommand object
using IDbConnection connection = this.Factory.Invoke();
using IDbCommand command = connection.CreateCommand();
command.CommandType = CommandType.Text;
command.CommandText = "SELECT * FROM WeatherInfo WHERE Date = @date";
command.Parameters.Clear();
command.Parameters.Add(new SqlParameter("@date", SqlDbType.DateTime)
{ Value = startDate });
//.... Rest of the code....
根据我们在方法中使用的操作,这可能会有所不同,但是使用指令创建IDbConnection实现是相同的:
using IDbConnection connection = this.Factory.Invoke();
在resume中创建我们的可测试类,操作如下:
实现测试代码
现在实现测试代码非常简单。我们只需要更改Mock对象的工厂实现,并替换和配置基于此初始模拟的所有对象。
XUnit代码中的主要步骤是创建IdbConnection模拟对象,如下一个代码段所示:
public class MoqSqlTest
{
readonly MoqReadyService service;
readonly Mock<IDbConnection> moqConnection;
public MoqSqlTest()
{
this.moqConnection = new Mock<IDbConnection>(MockBehavior.Strict);
moqConnection.Setup(x => x.Open());
moqConnection.Setup(x => x.Dispose());
this.service = new MoqReadyService(() => moqConnection.Object);
}
// Continue the code.....
在此代码段中,您可以观察到moq对象是如何基于IDbConnection测试的部分配置创建的。创建此基础对象后,其余测试的创建取决于您要测试的数据访问功能类型。让我们在下一节中看到这一点。
使用代码
该代码提供了两个测试类示例,它们测试从数据库读取和插入信息的方法。
使用Data Reader测试读取操作。
[Trait("DataReader", "1")]
[Fact(DisplayName = "DataReader Moq Set Strict Behaviour to Command Async")]
public async Task MoqExecuteReaderFromDatabaseAsync()
{
// Define the data reader, that return only one record.
var moqDataReader = new Mock<IDataReader>();
moqDataReader.SetupSequence(x => x.Read())
.Returns(true) // First call return a record: true
.Returns(false); // Second call finish
// Record to be returned
moqDataReader.SetupGet<object>(x => x["Date"]).Returns(DateTime.Now);
moqDataReader.SetupGet<object>(x => x["Summary"]).Returns("Sunny with Moq");
moqDataReader.SetupGet<object>(x => x["Temperature"]).Returns(32);
// Define the command to be mock and use the data reader
var commandMock = new Mock<IDbCommand>();
// Because the SQL to mock has parameter we need to mock the parameter
commandMock.Setup(m => m.Parameters.Add
(It.IsAny<IDbDataParameter>())).Verifiable();
commandMock.Setup(m => m.ExecuteReader())
.Returns(moqDataReader.Object);
// Now the mock if IDbConnection configure the command to be used
this.moqConnection.Setup(m => m.CreateCommand()).Returns(commandMock.Object);
// And we are ready to do the call.
List<WeatherForecast> result =
await this.service.GetForecastMoqableAsync(DateTime.Now);
Assert.Single(result);
commandMock.Verify(x => x.Parameters.Add(It.IsAny<IDbDataParameter>()),
Times.Exactly(1));
}
使用Mock behavior Strict测试Insert操作。
[Trait("ExecuteNonQuery", "1")]
[Fact(DisplayName = "Moq Set Strict Behaviour to Command Async")]
public async Task MoqExecuteNonQueryStrictBehaviourforCommandAsync()
{
WeatherForecast whetherForecast = new()
{
TemperatureC = 25,
Date = DateTime.Now,
Summary = "Time for today"
};
// Configure the mock of the command to be used
var commandMock = new Mock<IDbCommand>(MockBehavior.Strict);
commandMock.Setup(c => c.Dispose());
commandMock.Setup(c => c.ExecuteNonQuery()).Returns(1);
// Use sequence when several parameters are needed
commandMock.SetupSequence(m => m.Parameters.Add(It.IsAny<IDbDataParameter>()));
// You need to set this if use strict behaviour.
// Depend of your necessity for test
commandMock.Setup(m => m.Parameters.Clear()).Verifiable();
commandMock.SetupProperty<CommandType>(c => c.CommandType);
commandMock.SetupProperty<string>(c => c.CommandText);
// Setup the IdbConnection Mock with the mocked command
this.moqConnection.Setup(m => m.CreateCommand()).Returns(commandMock.Object);
// SUT
var result = await service.SetForecastAsync(whetherForecast);
Assert.Equal(1, result);
commandMock.Verify(x => x.Parameters.Add
(It.IsAny<IDbDataParameter>()), Times.Exactly(3));
}
请注意,在这种情况下,我们使用严格行为创建模拟对象,我们也可以使用松散行为创建它,行为的使用取决于您要在类中测试的内容。
松散的行为允许您创建更短的测试,但您可能会丢失有关您要在被测类中测试的内容的信息。
这是使用与上一个代码示例相同的类的松散行为示例:
[Trait("ExecuteNonQuery", "2")]
[Fact(DisplayName = "Moq Set Loose Behaviour to Command Async")]
public async Task MoqExecuteNonQuerySetLooseBehaviourToCommandAsync()
{
WeatherForecast whetherForecast = new()
{
TemperatureC = 25,
Date = DateTime.Now,
Summary = "Time for today"
};
// Configure the mock of the command to be used
var commandMock = new Mock<IDbCommand>(MockBehavior.Loose);
commandMock.Setup(c => c.ExecuteNonQuery()).Returns(1);
// Use sequence when several parameters are needed
commandMock.SetupSequence(m => m.Parameters.Add(It.IsAny<IDbDataParameter>()));
// Setup the IdbConnection Mock with the mocked command
this.moqConnection.Setup(m => m.CreateCommand()).Returns(commandMock.Object);
// SUT
var result = await service.SetForecastAsync(whetherForecast);
Assert.Equal(1, result);
commandMock.Verify(x => x.Parameters.Add
(It.IsAny<IDbDataParameter>()), Times.Exactly(3));
}
兴趣点
我发现一些开发人员倾向于使用数据库的简单操作,非常庞大的框架作为实体框架,理由如下:
- ADO.NET类不能进行单元测试
- ADO.NET无法进行异步操作
您可以下载的简单示例代码允许您对DB进行异步调用,并且还可以在没有EF开销的情况下对类进行单元测试。
我不反对EF,它在与DB的大而复杂的接口中非常有用,但是当与DB的所有交互都是几个请求或insert操作时,我更喜欢简单的ADO.NET操作。
我通常使用微服务,这就是我每天使用Db处理的情况。
https://www.codeproject.com/Articles/5332010/Unit-Test-Your-Database-Classes