如何在单元测试中伪造数据库操作?

How to fake database operations in Unit test?

我正在为我的 class 库编写单元测试,我正在尝试为一种方法编写测试。此方法进行一些数据库调用以从数据库中获取数据并将数据插入到某些表中。我希望这是假的。所以它应该像它在实际数据库表上做的那样,但实际上它不应该影响原始数据库。

我以前没有这样做过,但是我试过下面的方法。

private Mock<DBService> _dBService;
MyDLL _myDll;
public UnitTest1()
{
    _dBService = new Mock<DBService>();
    _myDll= new MyDLL (_dBService.Object);
}

[TestMethod]
public void TestMethod1()
{
    var response = _myDll.TestMethod(data);
    ...
}

public string TestMethod(List<long> data)
{
    var temp = _dbService.GetDataFromDB(data);
    ...
    ...
    _dbService.InsertIntoTable(someData);
}

我用MOQ伪造了DBService,因为所有数据库相关的方法都在DBService class.

中实现了

此外,如果我在测试中直接使用_dbService从数据库中获取数据,它returns null。但在 TestMethod.

内部调用时它会按预期工作
[TestMethod]
public void TestMethod1()
{
    var response = _dbService.GetDataFromDB(data); //returns null ?? why?
    ...
}

更新:添加 GetDataFromDB

的定义
public List<Transaction> GetDataFromDB(List<long> ids)
{
    XmlSerializer xmlSerializer = new XmlSerializer(ids.GetType());
    using (StringWriter textWriter = new StringWriter())
    {
        xmlSerializer.Serialize(textWriter, ids);
        string xmlId = textWriter.ToString();

        var parameters = new[]
        {
            new SqlParameter("@xml", DbType.String) { Value = xmlId }
        };

        return _dataAccess
            .CallProcedure<Transaction>("GetTransactionDetails", parameters).ToList();
    }
}

public class Transaction
{
    public long ID { get; set; }

    public double? Amount { get; set; }

    public long? CompanyId { get; set; }
}

我不明白你想用你的代码测试什么,但要解决你的问题,你需要这样做:

首先您需要在您的 DBService 中实现一个接口:

class DBService : IDBService

当然需要在这个接口中声明DBService的public个方法

然后你模拟接口而不是具体的 class:

  _dbService = Mock<IDBService>();

现在您可以设置方法了。

要解决第一个问题(插入真实数据库),您需要设置您的方法然后调用它的 mock:

  _dbService.Setup(x =>  ​x.InsertIntoTable(it...));
  _dbService.Object.InsertTable(...);

要解决第二个问题,你应该使用.Returns()方法传递你的模拟结果

  _dbService.Setup(x =>  ​x.GetDataFromDB(It.IsAny<List<long>>)).Returns(YourMockedResult);
  
  _dbService.Object.GetDataFromDB(...);

如果您对实现有任何疑问,文档中有很多示例:

https://github.com/Moq/moq4/wiki/Quickstart

更新

我尝试模拟您的场景并且这段代码工作正常(使用 .net 测试框架):

//Create an interface for your DBService
public interface IDBService
{
    List<Transaction> GetDataFromDB(List<long> ids);

    void InsertIntoTable(List<long> data);
}

//DLL class now receive a interface by dependency injection instead a concrete class
public class MyDLL
{
    private readonly IDBService _dbService;
    public MyDLL(IDBService dbService)
    {
        _dbService = dbService;
    }

    public string TestMethod(List<long> data)
    {
        var temp = _dbService.GetDataFromDB(data);//will be returned yourMockedData and assigned to temp

        _dbService.InsertIntoTable(data);

        //... rest of method implementation...

        return string.Empty; //i've returned a empty string because i don't know your whole method implementation
    }
}

//Whole method class implementation using .net test frameork
[TestClass]
public class UnitTest1
{
    private Mock<IDBService> _dbService;
    MyDLL _myDll;

    [TestInitialize]
    public void Setup()
    {
        _dbService = new Mock<IDBService>();
        
        //setup your dbService
        _dbService.Setup(x => x.InsertIntoTable(new List<long>()));

        //data that will be returned when GetDataFromDB get called
        var yourMockedData = new List<Transaction>
        {
            {
                new Transaction{ Amount = 1, CompanyId = 123, ID = 123}
            },
            {
                new Transaction{ Amount = 2, CompanyId = 321, ID = 124}
            }
        };

        _dbService.Setup(x => x.GetDataFromDB(new List<long>())).Returns(yourMockedData);

        //instantiate MyDll with mocked dbService
        _myDll = new MyDLL(_dbService.Object);
    }

    [TestMethod]
    public void TestMethod1()
    {
        //Testing MyDll.TestMethod()
        var response = _myDll.TestMethod(new List<long>());
        //Assert...Whatever do you want to test
    }

    [TestMethod]
    public void TestMethod2()
    {
        //Testing dbService deirectly
        var response = _dbService.Object.GetDataFromDB(new List<long>());
        //Assert...Whatever do you want to test
    }
}

另一种选择是使用内存数据上下文,其中真实数据库的模式在单元测试的内存中复制 像 Entity Framework 这样的 ORM(它是 .NET Framework 库的一部分)将您的查询和存储过程抽象为可测试的 类。这将清理您的数据访问 C# 代码并使其更易于测试。

要将您的测试数据上下文与 Entity Framework 一起使用,请尝试使用诸如 Entity Framework Effort (https://entityframework-effort.net/) or if you are using .NET Core, the built-in in-memory provider using EF Core. I have an example in my site where I use in-memory providers in .NET Core (http://andrewhalil.com/2020/02/21/using-in-memory-data-providers-to-unit-test-net-core-applications/).

之类的工具

直接测试后端 SQL 对象(来自测试数据库),如存储过程或大型数据集,与集成测试相同,这与您在此要实现的目标不同。在这种情况下,对您的单元测试和集成测试使用模拟来测试实际的后端存储过程。

让我把我的 2 美分放在这里。

根据我的经验,尝试 mock/replace 测试数据存储通常不是一个好主意,因为 mocks/in-memory 数据库与真实数据库相比存在局限性和行为差异。

此外,由于断言的方式,模拟通常会导致脆弱的测试,因为您基本上依赖于被测系统的实现细节,而不是仅检查其 public 表面。通过断言“此服务在执行此方法后调用另一个服务 2 次”之类的内容,您可以让您的测试了解逻辑,而它们不应该知道。这是一个内部细节,可能是一个经常变化的主题,所以任何应用程序更新都会导致测试失败,而逻辑仍然是正确的。而且大量的测试误报肯定不好。

我宁愿建议您使用真实的数据库进行测试,这需要访问数据存储,同时尽量减少此类测试的数量。大多数逻辑应该由单元测试覆盖。它可能需要对应用程序设计进行一些更改,但这些通常是永久性的。

然后有不同的技巧来实现测试的最佳性能,它们使用真实的数据库:

  • 将改变数据的测试与只读测试分开,并在不进行任何数据清理的情况下并行启动后者;
  • 使用唯一优化的脚本insert/delete测试数据。您可能会发现 Reseed library I'm developing currently helpful for that aim. It's able to generate both insert and delete scripts for you and execute those fast. Or check out Respawn 可用于数据库清理;
  • 使用数据库快照进行恢复,这可能比完整 insert/delete 周期更快;
  • 将每个测试包装在事务中并在之后还原;
  • 通过使用数据库池而不是唯一的数据库池来并行化您的测试。 Docker 和 TestContainers 可能适合这里;