数据访问层单元测试的方法

20
我一直在寻找一种有效的方法来单元测试C#中的数据访问层。我主要是Java开发人员,只使用C#约6个月,在过去,我使用了一个叫做DBUnit的库来测试已知状态数据库。我还没有找到类似的活跃库,最接近的似乎是nDBUnit,但它已经有一段时间没有更新了。
在C#中,似乎存在很多矛盾的方法和原因。理想情况下,我希望使用模拟测试数据访问层,而不需要连接到数据库,然后在单独的测试集中对存储过程进行单元测试。
在我正在处理的系统中,数据访问层使用ADO.net(不使用Entity Framework)来调用SQL Server上的存储过程。
下面是一个示例代码,我需要使用模拟对象来模拟SqlCommand(使用IDbCommand)和/或模拟SqlConnection。
因此我的问题是,如果有这样的东西,最好的方法似乎是什么?到目前为止,唯一的方法是创建代理对象,该对象传递到构造函数中,以便可以返回模拟的Sql*对象进行测试。

我还没有机会查看所有可用的C#模拟库。

public class CustomerRepository : ICustomerRepository
{
   private string connectionString;

   public CustomerRepository (string connectionString)
   {
     this.connectionString = connectionString;
   }

   public int Create(Customer customer)
   {

     SqlParameter paramOutId = new SqlParameter("@out_id", SqlDbType.Int);
     paramOutId.Direction = ParameterDirection.Output;
     List<SqlParameter> sqlParams = new List<SqlParameter>()
     {
       paramOutId,
       new SqlParameter("@name", customer.Name)
     }

     SqlConnection connection = GetConnection();
     try
     {
       SqlCommand command = new SqlCommand("store_proc_name", connection);

       command.CommandType = CommandType.StoredProcedure;

       command.Parameters.AddRange(sqlParams.ToArray());

       int results = command.ExecuteNonQuery();

       return (int) paramOutId.Value;
     }
     finally
     {
       CloseConnection(connection);
     }

   }

}
4个回答

27
很遗憾你找不到一个工具可以将数据库放入已知状态,然后运行CustomerRepository针对数据库进行测试。但是,答案并不是开始使用模拟来模仿所有ADO调用。这样做会创建一个单元测试,它实际上并没有测试任何逻辑:它只是测试代码是否按照你认为应该编写的方式编写。
假设我最终编写了一个SQL INSERT作为在SQL数据库中创建客户的命令。现在假设我们正在进行更改,以便客户表具有不同的字段(这会破坏我们的INSERT命令),并且现在我们应该使用存储过程创建客户。使用模拟测试仍将通过,即使它测试的实现现在已经损坏。此外,如果您修复了实现以使用存储过程,则您的单元测试现在将失败。如果单元测试在应该失败时继续通过,但在修复系统后会失败,那么单元测试的意义是什么?
参见此问题以了解可能的替代方案。看起来标记的答案实际上是在C#中使用IKVM使用DBUnit。
因此,可能还有其他替代渠道需要继续探索,但是模拟ADO调用只会导致脆弱的测试,而且不会真正测试任何重要内容。

谢谢,我在周末仔细研究了一下,我同意测试应该访问真实的数据库,在我们的Java项目中,它一直像这样工作,特别是随着项目的增长,表和列名已经发生了变化。我看了一下IKM方法,我不想引入任何太复杂的东西,以便其他开发人员能够管理和理解。 - wenic
确切地说,这就是为什么您应该创建一个使用存储库/DAL的服务。您可以模拟存储库,将其注入到服务中并测试该服务。然后,您可以在隔离环境中测试服务中的逻辑。如果存储库出现问题(返回不正确的数据),则逻辑将失败并且测试也会失败。看到人们模拟存储库,然后断言从该存储库返回的内容总是很有趣的。这样做并没有测试任何东西。 - Gaui

3

这一层的工作是将代码与数据库连接起来。它必须封装关于数据库连接和语法的知识。通常情况下,它将领域语言映射到数据库语言。我认为这部分单元测试是集成测试,因此我测试数据库架构是否等同于真实或测试数据库。更多相关信息请参见这里


谢谢,我想我过于深入研究了这个问题,最初我想用C#实现这个功能,但由于缺乏相关库,我开始寻找替代方案。 - wenic

1
为了测试数据访问层,您需要一个更复杂的结构。
数据访问层将调用存储库对象的引用。 Repo对象将通过UnitOfWork设计模式调用Entity Framework DbSets的引用。
数据访问层(顶层)
| UnitOfWork
| 存储库模式类
| EF上下文
| 实际数据库
设置结构后,您将模拟存储库类。例如,项目将插入到数据库中而不是进入模拟对象。稍后,您将根据模拟对象进行断言,以查看是否插入了项目。
请查看实现存储库和工作单元模式

0

编写单元测试用例时,您需要使用DB接口方法并模拟这些接口方法。我们无法模拟预定义类,因此必须使用接口。如果您认为需要任何命令或数据阅读器实例应该是预定义类,则编写一个接口方法,在其中将接口实例强制类型转换为类类型,使用类属性,然后再次返回接口实例。例如,如果您需要oracleCommand,请创建IDBCommand并将IDBCommand传递给接口,并更改类型,使用它并返回它。

示例代码,

 public interface IDbConnectionFactory
    {
        IDbConnection GetConnection();
        IDbCommand BindByName(IDbCommand dbCommand);
    }


public class DbConnectionFactory : IDbConnectionFactory
{

    public IDbConnection GetConnection()
    {
        try
        {
            var connection = new SQLConnection();

            var connection = connection.CreateConnection();
            if (connection != null)
            {
                connection.ConnectionString = _connectionString;
                return connection;
            }
        }
        catch (Exception e)
        {
            Console.WriteLine(e.Message);
        }
        return null;
    }

    public IDbCommand BindByName(IDbCommand dbCommand)
    {
        var command = (SQLCommand)dbCommand;
        command.BindByName = true;
        return command;
    }
}

//现在编写您的数据库测试用例

public void CustomerRepository_Create_Returns_Int()
{

    var customer = new Customer(); //Initialise values of customer here
    var _refCustomerRepository = new CustomerRepository();
    var _mockDBConnection = new Mock<IDbConnection>();

    var _mockDBCommand = new Mock<IDbCommand>();

    var _mockDBConnectionFactory = new Mock<IDbConnectionFactory>();

   

    _mockDBConnectionFactory.Setup(c =>
        c.GetConnection()).Returns(_mockDBConnection.Object);

    _mockDBConnection.Setup(m => 
        m.CreateCommand()).Returns(_mockDBCommand.Object);

    // for out value. Rename "@out_id" to "out_id". 
    _mockDBCommand.Setup(m => m.Parameters.Add(ParameterDirection.Output));
    // Rename ur paramter from "@name" to "name", it'll work
    _mockDBCommand.Setup(m => m.Parameters.Add("name"));

    _mockDBCommand.Setup(m => m.Parameters.AddRange(5));

    _mockDBCommand.Setup(m => m.ExecuteNonQuery()).Returns(1);

    var result = _refCustomerRepository.Create(saveCustomerSettings);

    //  Assert your result here

}

// 用于测试执行读取器

var _mockDataReader = new Mock<IDataReader>();

    _mockDataReader.SetupSequence(_ => _.Read())
                      .Returns(true)
                      .Returns(false);

// Do this for all columns that a query/SP returns 
   _mockDataReader.Setup(x => x["COLUMN_NAME"]).Returns("RETURN_VALUE");
   _mockDataReader.Setup(x => x["COLUMN_NAME"]).Returns("RETURN_VALUE"); 
   _mockDataReader.Setup(x => x["COLUMN_NAME"]).Returns("RETURN_VALUE");

例如,如果返回具有FIRST_NAME、LAST_NAME和EMAIL属性的客户

   _mockDataReader.Setup(x => 
            x["FIRST_NAME"]).Returns("giveAnyValueYouWantThisColumnToReturn");
   _mockDataReader.Setup(x => 
            x["LAST_NAME"]).Returns("giveAnyValueYouWantThisColumnToReturn"); 
   _mockDataReader.Setup(x => 
            x["EMAIL"]).Returns("giveAnyValueYouWantThisColumnToReturn");

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接