Skip to main content
Version: 2.4.1

Testing with RCommon

RCommon is designed with testability as a first-class concern. Because RCommon wraps infrastructure concerns — persistence, messaging, caching, emailing — behind clean abstractions, you can substitute real implementations with mocks or in-memory alternatives in your test suite without changing your application code.

Testing Philosophy​

RCommon separates infrastructure concerns from domain logic through interfaces. A service that writes to a repository via IGraphRepository<T> does not know whether it is talking to SQL Server, an in-memory database, or a mock. The same is true for IEmailService, ICacheService, IMediatorService, and all other infrastructure services.

This makes three testing strategies practical:

  • Unit tests — mock infrastructure interfaces with Moq; test business logic in isolation with no external dependencies.
  • Integration tests — use in-memory providers (EF Core InMemory, IDistributedCache in-memory) to test the full stack cheaply.
  • End-to-end tests — wire real providers against a test database or container; RCommon's base classes handle bootstrapping.

When to Use Each Strategy​

StrategyWhen to UseTools
Unit testsTesting a single class in isolationMoq, FluentAssertions, xUnit
Integration testsTesting a stack of components including DI and the persistence layerRCommon.TestBase, EF Core InMemory, xUnit
End-to-end testsTesting against real infrastructure in CIRCommon.TestBase, SQL Server, Docker

Mocking RCommon Abstractions​

Because all RCommon services are registered as interfaces in the DI container, they are straightforward to mock with Moq.

Mocking a Repository​

using Moq;
using RCommon.Persistence.Crud;
using FluentAssertions;

public class OrderServiceTests
{
private readonly Mock<IGraphRepository<Order>> _mockOrderRepo;
private readonly Mock<IUnitOfWorkFactory> _mockUoWFactory;
private readonly OrderService _sut;

public OrderServiceTests()
{
_mockOrderRepo = new Mock<IGraphRepository<Order>>();
_mockUoWFactory = new Mock<IUnitOfWorkFactory>();
_sut = new OrderService(_mockOrderRepo.Object, _mockUoWFactory.Object);
}

[Fact]
public async Task GetOrderAsync_WhenOrderExists_ReturnsOrder()
{
// Arrange
var expected = new Order { Id = 1, ProductName = "Widget" };
_mockOrderRepo
.Setup(r => r.FindAsync(1, default))
.ReturnsAsync(expected);

// Act
var result = await _sut.GetOrderAsync(1);

// Assert
result.Should().BeEquivalentTo(expected);
}
}

Mocking an HTTP Client​

The TestBootstrapper base class provides a CreateMockHttpClient helper for testing services that depend on IHttpClientFactory:

using System.Net;
using System.Net.Http;
using RCommon.TestBase;
using Moq;

public class ExternalApiServiceTests : TestBootstrapper
{
[Test]
public async Task FetchData_WhenServerReturns500_ReturnsNull()
{
// Arrange
var mockResponse = new HttpResponseMessage
{
StatusCode = HttpStatusCode.InternalServerError
};
var mockFactory = CreateMockHttpClient(mockResponse);

var service = new ExternalApiService(mockFactory.Object);

// Act
var result = await service.FetchDataAsync();

// Assert
Assert.That(result, Is.Null);
}
}

Mocking Time-Dependent Code​

ISystemTime abstracts the system clock, making any code that uses it testable:

using Moq;
using RCommon;

var mockTime = new Mock<ISystemTime>();
mockTime.Setup(t => t.Now).Returns(new DateTime(2024, 6, 15, 12, 0, 0));

var service = new SubscriptionRenewalService(mockTime.Object, /* ... */);

Integration Testing with In-Memory Providers​

For integration tests, use EF Core's InMemory database to test the full persistence stack without a real database:

using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using RCommon.Persistence.EFCore;
using Xunit;

public class OrderRepositoryIntegrationTests : IDisposable
{
private readonly ServiceProvider _serviceProvider;

public OrderRepositoryIntegrationTests()
{
var services = new ServiceCollection();
services.AddLogging();
services.AddSingleton<IEntityEventTracker, InMemoryEntityEventTracker>();

var builder = new EFCorePerisistenceBuilder(services);
builder.AddDbContext<AppDbContext>("AppDb", options =>
options.UseInMemoryDatabase(Guid.NewGuid().ToString()));
builder.SetDefaultDataStore(o => o.DefaultDataStoreName = "AppDb");

_serviceProvider = services.BuildServiceProvider();
}

public void Dispose() => _serviceProvider?.Dispose();

[Fact]
public async Task AddAsync_PersistsEntity()
{
// Arrange
var repo = _serviceProvider.GetRequiredService<IGraphRepository<Order>>();
var order = new Order { ProductName = "Widget", Quantity = 5 };

// Act
await repo.AddAsync(order);

// Assert
var retrieved = await repo.FindAsync(order.Id);
retrieved.Should().NotBeNull();
retrieved!.ProductName.Should().Be("Widget");
}
}

Using the xUnit TestFixture Base Class​

For xUnit projects, TestFixture (from RCommon.TestBase.XUnit) provides DI bootstrapping, configuration loading, and service resolution in a single base class:

using RCommon.TestBase.XUnit;
using Microsoft.Extensions.DependencyInjection;

public class ProductServiceTests : TestFixture
{
protected override void ConfigureServices(IServiceCollection services)
{
base.ConfigureServices(services);

// Add your services and mocks
services.AddSingleton<Mock<IProductRepository>>();
services.AddTransient<ProductService>();
}

[Fact]
public void ProductService_CanBeResolved()
{
var service = GetService<ProductService>();
service.Should().NotBeNull();
}
}

Event Handling in Tests​

Test event handling by subscribing to the in-memory event bus:

using RCommon.EventHandling;
using Microsoft.Extensions.DependencyInjection;

var services = new ServiceCollection();
services.AddRCommon(builder =>
{
builder.WithEventHandling<InMemoryEventBusBuilder>(events =>
{
events.AddSubscriber<OrderCreatedEvent, OrderCreatedEventHandler>();
});
});

var provider = services.BuildServiceProvider();
var eventBus = provider.GetRequiredService<IEventBus>();

// Publish and verify the handler was called
await eventBus.Publish(new OrderCreatedEvent { OrderId = Guid.NewGuid() });

Testing CQRS Handlers​

Test command and query handlers directly by instantiating them with mock dependencies, bypassing the bus:

public class CreateOrderHandlerTests
{
private readonly Mock<IGraphRepository<Order>> _mockRepo;
private readonly CreateOrderHandler _sut;

public CreateOrderHandlerTests()
{
_mockRepo = new Mock<IGraphRepository<Order>>();
_sut = new CreateOrderHandler(_mockRepo.Object);
}

[Fact]
public async Task HandleAsync_ValidCommand_AddsOrder()
{
// Arrange
var command = new CreateOrderCommand { ProductName = "Widget", Quantity = 3 };

// Act
var result = await _sut.HandleAsync(command, CancellationToken.None);

// Assert
result.IsSuccess.Should().BeTrue();
_mockRepo.Verify(r => r.AddAsync(It.IsAny<Order>(), default), Times.Once);
}
}

Summary​

RCommon's abstraction boundaries make all major infrastructure concerns testable:

  • Repository operations via IReadOnlyRepository<T>, IWriteOnlyRepository<T>, ILinqRepository<T>, IGraphRepository<T>, and ISqlMapperRepository<T>
  • Unit of work and transactions via IUnitOfWork and IUnitOfWorkFactory
  • Event handling via IEventBus, ISubscriber<T>, and IEventProducer
  • Mediator operations via IMediatorService
  • Email sending via IEmailService
  • Caching via ICacheService
  • Time-dependent logic via ISystemTime
  • HTTP clients via IHttpClientFactory (with the built-in mock helper on TestBootstrapper)
RCommonRCommon