简体   繁体   English

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

[英]How to fake database operations in Unit test?

I am writing the unit tests for my class library, There is a method for which I am trying to write tests.我正在为我的 class 库编写单元测试,有一种我正在尝试编写测试的方法。 this method makes some database calls to get data from the database and inserts data into some tables.此方法进行一些数据库调用以从数据库中获取数据并将数据插入某些表中。 I want this to be fake.我希望这是假的。 So it should like it is doing on actual database tables but actually it should not affect the original database.所以它应该像在实际数据库表上做的那样,但实际上它不应该影响原始数据库。

I have not done this before, But I have tried the below way.我以前没有这样做过,但是我尝试了以下方法。

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);
}

I have used MOQ to fake the DBService , because all the database-related methods are implemented in the DBService class.我使用MOQ来伪造DBService ,因为所有与数据库相关的方法都在DBService class 中实现。

Also, If I try to get data from the database by directly using the _dbService in the test, it returns null .另外,如果我尝试在测试中直接使用_dbService从数据库中获取数据,它会返回null but it works as expected when it is called inside the TestMethod .但是在TestMethod内部调用它时,它会按预期工作。

[TestMethod]
public void TestMethod1()
{
    var response = _dbService.GetDataFromDB(data); //returns null ?? why?
    ...
}

Update : Adding definition of GetDataFromDB更新:添加 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; }
}

I'm didn't understand what you want to test with your code, but to solve your problem you need to do this:我不明白你想用你的代码测试什么,但要解决你的问题,你需要这样做:

First you need to implement an interface in your DBService:首先你需要在你的 DBService 中实现一个接口:

class DBService: IDBService

Of course you need to declare the public methods of DBService in this interface.当然你需要在这个接口中声明 DBService 的公共方法。

Then you mock the interface instead concrete class:然后你模拟接口而不是具体的 class:

  _dbService = Mock<IDBService>();

Now you can setup your methods.现在你可以设置你的方法了。

To solve the first problem (inserting in real database) you setup your method and then call it's mock:要解决第一个问题(插入真实数据库),您需要设置方法,然后将其称为模拟:

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

To solve the second problem, you should use .Returns() method passing your Mock result要解决第二个问题,您应该使用.Returns()方法传递您的 Mock 结果

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

If you're any doubt about the implementation, here in docs have a lot of examples:如果您对实现有任何疑问,这里的文档中有很多示例:

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

Update更新

I tryied to simulate your scenario and this code works fine (using .net test framework):我试着模拟你的场景,这段代码工作正常(使用 .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
    }
}

Another alternative is to use an in-memory data context where schema for your real database is replicated in-memory of your unit test with an ORM like Entity Framework (which is part of the .NET Framework libraries) to abstract your queries and stored procedures into testable classes.另一种选择是使用内存数据上下文,其中真实数据库的模式在单元测试的内存中复制,使用 ORM 之类的实体框架(它是 .NET 框架库的一部分)将您的查询和存储过程抽象为可测试的类。 This would clean up your data access C# code and make it more testable.这将清理您的数据访问 C# 代码并使其更具可测试性。

To use your test data context with Entity Framework, try a tool such as Entity Framework Effort ( https://entityframework-effort.net/ ) or if you are using .NET Core, the built-in in-memory provider using EF Core.要将您的测试数据上下文与 Entity Framework 一起使用,请尝试使用 Entity Framework Effort ( https://entityframework-effort.net/ ) 之类的工具,或者如果您使用的是 .NET Core,这是使用 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/ ).我在我的站点中有一个示例,我在 .NET Core 中使用内存提供程序( http://andrewhalil.com/2020/02/21/using-in-memory-data-providers-to-unit-test-net-核心应用程序/ )。

Directly testing backend SQL objects (from test databases) like stored procedures or large datasets is the same as integration testing, which is different from what you are aiming to achieve here.直接测试后端 SQL 对象(来自测试数据库)如存储过程或大型数据集与集成测试相同,这与您在这里的目标不同。 In this case use mocking for your unit tests and integration testing to test the actual backend stored procedure.在这种情况下,使用 mocking 进行单元测试和集成测试,以测试实际的后端存储过程。

Let me put my 2 cents in here.让我把我的 2 美分放在这里。

From my experience, attemtps to mock/replace a data storage for tests is usually not a good idea, due to the limitations and behavior differences of the mocks/in-memory databases in comparison to the real database.根据我的经验,由于模拟/内存数据库与真实数据库相比的局限性和行为差异,尝试模拟/替换测试数据存储通常不是一个好主意。

Moreover mocks usually lead to fragile tests due to the way assertions are made, as you basically depend on the implementation details of the system under tests instead of examining just its public surface.此外,由于进行断言的方式,模拟通常会导致脆弱的测试,因为您基本上依赖于被测系统的实现细节,而不仅仅是检查其公共表面。 By asserting things like "this service called another service 2 times after this method is executed" you are making your tests aware of logic, which they shouldn't be aware of.通过断言诸如“此服务在执行此方法后两次调用另一个服务”之类的内容,您可以使您的测试了解逻辑,而他们不应该了解这些逻辑。 It's an internal detail, which is possibly a subject of frequent changes, so any application update will lead to test failures, while the logic is still correct.这是一个内部细节,可能是频繁更改的主题,因此任何应用程序更新都会导致测试失败,而逻辑仍然是正确的。 And high amount of tests false positives isn't surely good.大量的测试误报也不一定是好事。

I'd rather recommend that you use a real database for tests, which need access to data storage, while trying to keep a quantity of such tests minimal.我宁愿建议您使用真正的数据库进行测试,这需要访问数据存储,同时尽量减少此类测试的数量。 Most of the logic should be covered by unit tests instead.大多数逻辑应该由单元测试覆盖。 It might require some application design changes, but those are usually for good.它可能需要一些应用程序设计更改,但这些通常是好的。

And there are different tricks then to achieve the best performance of tests, which are using a real database:然后有不同的技巧来实现测试的最佳性能,它们使用真实的数据库:

  • Separate tests, which mutate data, from the read-only ones and start the latter in parallel without any data cleanup;将改变数据的测试与只读测试分开,并在没有任何数据清理的情况下并行启动后者;
  • Use the only optimized script to insert/delete test data.使用唯一优化的脚本来插入/删除测试数据。 You might find Reseed library I'm developing currently helpful for that aim.您可能会发现我目前正在开发的Reseed库有助于实现这一目标。 It's able to generate both insert and delete scripts for you and execute those fast.它能够为您生成插入和删除脚本并快速执行。 Or check out Respawn which could be used for database cleanup;或者查看可用于数据库清理的Respawn
  • Use database snapshots for the restore, which might be faster than full insert/delete cycle;使用数据库快照进行还原,这可能比完整的插入/删除周期更快;
  • Wrap each test in transaction and revert it afterwards;将每个测试包装在事务中,然后将其还原;
  • Parallelize your tests by using a pool of databases instead of the only.通过使用数据库池而不是唯一的来并行化您的测试。 Docker and TestContainers could be suitable here; Docker 和TestContainers可能适合这里;

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

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