我一直在研究项目数据访问层的单元测试。大多数选项归结为以下几点:
- 使用专用测试数据库,但需要在所有单元测试的结束阶段进行清理(或手动清理)
- 使用数据库,但不提交或仅回滚
- 模拟数据库
我更喜欢使用测试数据库而不是“不提交”想法。
我的开发数据库中包含虚拟记录或完全经过清洗的生产数据样本。
我的集成测试数据库是实际生产数据库的副本(这个版本用于在我将更改实时发布之前进行测试)。
[SetUp]
public void SetUp()
{
Database.Clear();
manager = new ServiceManager();
}
[TearDown]
public void TearDown()
{
manager.Dispose();
}
[Test]
public void InsertAndLoadOrderContract()
{
MinOrderModel model = new OrderBuilder().InsertMinimalOrder(manager);
Contract contract = TestObjectGenerator.GenerateEntity<Contract>();
manager.Contract.InsertOrderContract(model.Order.OrderCompositeId, contract);
Contract selectedContract = manager.Contract.SelectById(contract.ContractId);
AssertContract(selectedContract, contract);
}
private static void AssertContract(IContract actual, IContract expected)
{
Assert.That(actual.AgencyCodeOther, Is.EqualTo(expected.AgencyCodeOther));
Assert.That(actual.AgencyFK, Is.EqualTo(expected.AgencyFK));
Assert.That(actual.ContractId, Is.EqualTo(expected.ContractId));
Assert.That(actual.Ident, Is.EqualTo(expected.Ident));
}
这个测试的某些部分可以替换为更方便的内容:
我想模拟数据库。在测试中处理数据库很痛苦,需要创建数据库、创建模式,然后删除它,确保没有挂起的连接等都很麻烦。
另一个让我感到不舒服的事情是验证代码逻辑与代码“相距甚远”。我会选择将 Sql 函数(连接、命令等)放在可模拟的类后面,并验证 DAL 是否调用了正确的方法。此外,这种方式能够大大加快测试的运行速度。
以下是一些快速的 SQL 抽象类和示例用法+单元测试。
public class SqlConnectionBase : IDisposable {
private readonly SqlConnection m_Connection;
public SqlConnectionBase(string connString) {
m_Connection = new SqlConnection(connString);
}
public virtual SqlConnection Object { get { return m_Connection; } }
public virtual void Open() {
m_Connection.Open();
}
public virtual void Close() {
m_Connection.Close();
}
#region IDisposable Members
public virtual void Dispose() {
m_Connection.Dispose();
}
#endregion
}
public class SqlCommandBase : IDisposable{
private readonly SqlCommand m_Command;
public SqlCommandBase() {
m_Command = new SqlCommand();
}
public SqlCommandBase(string cmdText, SqlConnectionBase connection) {
m_Command = new SqlCommand(cmdText, connection.Object);
}
public SqlCommandBase(SqlConnectionBase connection) {
m_Command = new SqlCommand();
m_Command.Connection = connection.Object;
}
public virtual int ExecuteNonQuery() { return m_Command.ExecuteNonQuery(); }
public virtual string CommandText { get { return m_Command.CommandText; } set { m_Command.CommandText = value; } }
public virtual void AddParameter(SqlParameter sqlParameter) {
m_Command.Parameters.Add(sqlParameter);
}
#region IDisposable Members
virtual public void Dispose() {
m_Command.Dispose();
}
#endregion
}
public class SqlFactory {
public virtual SqlCommandBase CreateCommand(string query, SqlConnectionBase conn) {
return new SqlCommandBase(query, conn);
}
public virtual SqlCommandBase CreateCommand(SqlConnectionBase conn) {
return new SqlCommandBase(conn);
}
public virtual SqlConnectionBase CreateConnection(string connString) {
return new SqlConnectionBase(connString);
}
}
public class DBUser {
public DBUser(SqlFactory factory) {
m_factory = factory; //dependency constructor, will be used during unit testing
}
public DBUser() {
m_factory = new SqlFactory(); //used during normal execution
}
public void DoSomething() {
var conn = m_factory.CreateConnection("server=servername,database=...");
var cmd = m_factory.CreateCommand(conn);
cmd.CommandText = "Select * from users";
cmd.ExecuteNonQuery();
}
[TestMethod]
public void DoSomethingTest() {
var factoryMock = new Mock<SqlFactory>();
var cmdMock = new Mock<CommandBase>();
factoryMock.Setup(f=>f.CreateConnection(It.IsAny<string>())).Returns(cmdMock.Object);
DBUser dbUser = new DBUser(factoryMock.Object);
dbUser.DoSomething();
//Verify that DoSomething is called.
cmdMock.Verify(c=>c.DoSomething());
}
}
System.Sql.Abstractions
库,类似于 System.IO.Abstractions。 - Igor Zevaka