When building a project that relies heavily on an external API, it can become challenging to create unit tests if the API is deeply integrated throughout the project. This scenario often leads developers to consider mocking and stubbing as a solution. However, there’s a balance to be struck between using mocks and stubs and relying on a testing database to achieve effective and maintainable tests.

The Challenge of Mocking and Stubbing

In a recent project, I aimed to construct unit tests in a way that allowed the entire project to be tested without a connection to a database. This was achieved by stubbing and mocking all custom API objects and actions. The Moq library, combined with xUnit, was employed to build the test cases for this .NET Class Library project.

using Moq;
using Xunit;

public class MyServiceTests
{
    [Fact]
    public void TestMethod()
    {
        // Arrange
        var mockProvider = new Mock<CustomAPIClass>();
        mockProvider.Setup(provider => provider.GetData()).Returns(new List<string> { "Mocked Data" });

        var myService = new MyService(mockProvider.Object);

        // Act
        var result = myService.ProcessData();

        // Assert
        Assert.NotNull(result);
        Assert.Equal("Processed Mocked Data", result);
    }
}

In most cases, several providers had to be mocked per method and class because the project extensively used custom objects and methods from the API. This added a layer of complexity to both the test suite and the production code.

Maintaining Readability and Maintainability

One of my guiding principles in development is to keep the code easily maintainable. This means any developer, including myself in the future, should be able to understand and modify the code without a steep learning curve.

However, the initial goal of simplifying testing and ensuring that changes wouldn’t break existing functionality backfired. The extensive use of mocks and providers for dependency injection made the codebase more complex and harder to follow.

Considering a Testing Database

An alternative approach is to use a testing database, thereby relying on the API as it was built. Setting up a database for testing might seem more complicated, but it allows the test suite to mirror actual use cases and user stories more closely. Instead of recreating method responses and potential scenarios, the tests interact with the API in its intended manner.

Example of Using a Testing Database

Here’s a simplified example demonstrating how to set up and use a testing database in your tests:

using Microsoft.EntityFrameworkCore;
using Xunit;

public class MyServiceIntegrationTests
{
    private readonly DbContextOptions<MyDbContext> _dbContextOptions;

    public MyServiceIntegrationTests()
    {
        _dbContextOptions = new DbContextOptionsBuilder<MyDbContext>()
            .UseInMemoryDatabase(databaseName: "TestDatabase")
            .Options;
    }

    [Fact]
    public void TestMethod()
    {
        // Arrange
        using (var context = new MyDbContext(_dbContextOptions))
        {
            context.MyEntities.Add(new MyEntity { Id = 1, Name = "Test Entity" });
            context.SaveChanges();
        }

        // Act
        using (var context = new MyDbContext(_dbContextOptions))
        {
            var service = new MyService(context);
            var result = service.GetEntity(1);

            // Assert
            Assert.NotNull(result);
            Assert.Equal("Test Entity", result.Name);
        }
    }
}

Choosing the Right Approach

The decision between using mocks and stubs versus a testing database often depends on the specific project. Key considerations include what will make the code most readable, maintainable, and understandable in the long term.

When to Use Mocks and Stubs

  • For isolated unit tests where the focus is on individual components.
  • When external dependencies are unreliable or unavailable.
  • To simulate edge cases and specific scenarios that are hard to reproduce.

When to Use a Testing Database

  • For integration tests that ensure various components work together as expected.
  • To validate actual interactions with the database and API.
  • When it’s crucial to mirror real-world use cases closely.

Both approaches have their merits and can be used complementarily. By carefully assessing the needs of your project, you can strike the right balance, ensuring your code remains both testable and maintainable.