繁体   English   中英

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

[英]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从数据库中获取数据,它会返回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 的公共方法。

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

  _dbService = Mock<IDBService>();

现在你可以设置你的方法了。

要解决第一个问题(插入真实数据库),您需要设置方法,然后将其称为模拟:

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

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

  _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
    }
}

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

要将您的测试数据上下文与 Entity Framework 一起使用,请尝试使用 Entity Framework Effort ( https://entityframework-effort.net/ ) 之类的工具,或者如果您使用的是 .NET Core,这是使用 EF Core 的内置内存提供程序。 我在我的站点中有一个示例,我在 .NET Core 中使用内存提供程序( http://andrewhalil.com/2020/02/21/using-in-memory-data-providers-to-unit-test-net-核心应用程序/ )。

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

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

根据我的经验,由于模拟/内存数据库与真实数据库相比的局限性和行为差异,尝试模拟/替换测试数据存储通常不是一个好主意。

此外,由于进行断言的方式,模拟通常会导致脆弱的测试,因为您基本上依赖于被测系统的实现细节,而不仅仅是检查其公共表面。 通过断言诸如“此服务在执行此方法后两次调用另一个服务”之类的内容,您可以使您的测试了解逻辑,而他们不应该了解这些逻辑。 这是一个内部细节,可能是频繁更改的主题,因此任何应用程序更新都会导致测试失败,而逻辑仍然是正确的。 大量的测试误报也不一定是好事。

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

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

  • 将改变数据的测试与只读测试分开,并在没有任何数据清理的情况下并行启动后者;
  • 使用唯一优化的脚本来插入/删除测试数据。 您可能会发现我目前正在开发的Reseed库有助于实现这一目标。 它能够为您生成插入和删除脚本并快速执行。 或者查看可用于数据库清理的Respawn
  • 使用数据库快照进行还原,这可能比完整的插入/删除周期更快;
  • 将每个测试包装在事务中,然后将其还原;
  • 通过使用数据库池而不是唯一的来并行化您的测试。 Docker 和TestContainers可能适合这里;

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM