简体   繁体   中英

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. 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.

Also, If I try to get data from the database by directly using the _dbService in the test, it returns null . but it works as expected when it is called inside the TestMethod .

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

Update : Adding definition of 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:

class DBService: IDBService

Of course you need to declare the public methods of DBService in this interface.

Then you mock the interface instead concrete 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

  _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

Update

I tryied to simulate your scenario and this code works fine (using .net test framework):

//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. This would clean up your data access C# code and make it more testable.

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. 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/ ).

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. In this case use mocking for your unit tests and integration testing to test the actual backend stored procedure.

Let me put my 2 cents in here.

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. 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;
  • 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;

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

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