I have a OfflineHourlyDatabaseBackup
BackgroundService
in .NET 8
as shown below.
OfflineHourlyDatabaseBackup
:
public class OfflineHourlyDatabaseBackup(
ILogger<OfflineHourlyDatabaseBackup> logger,
TimeProvider timeProvider,
IServiceProvider serviceProvider) : BackgroundService
{
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
await Task.Yield();
timeProvider.CreateTimer(Backup, null, TimeSpan.Zero, TimeSpan.FromHours(1));
stoppingToken.Register(() => logger.LogWarning($"{nameof(OfflineHourlyDatabaseBackup)} is stopping due to host shut down."));
}
private void Backup(object? state)
{
using var scope = serviceProvider.CreateScope();
var offlineDatabaseBackupService = scope.ServiceProvider.GetRequiredService<IOfflineDatabaseBackupService>();
offlineDatabaseBackupService.Backup();
}
}
I have written uni test to test the behavior. Here is my xunit
unit test,
[Fact]
public async Task OfflineHourlyDatabaseBackup_ExecuteAsync()
{
// Arrange
var serviceCollection = new ServiceCollection();
var mockOfflineDatabaseBackupService = new Mock<IOfflineDatabaseBackupService>();
serviceCollection.AddSingleton(_ => mockOfflineDatabaseBackupService.Object);
var now = DateTimeOffset.UtcNow;
var timeProvider = new TestTimeProvider(now);
var serviceProvider = serviceCollection.BuildServiceProvider();
var logger = NullLogger<OfflineHourlyDatabaseBackup>.Instance;
var configuration = new Mock<IConfiguration>();
// Act
using var offlineHourlyDatabaseBackup = new OfflineHourlyDatabaseBackup(logger, timeProvider, serviceProvider);
await offlineHourlyDatabaseBackup.StartAsync(default);
await offlineHourlyDatabaseBackup.ExecuteTask!; // --> Backup() should be called
timeProvider.Advance(TimeSpan.FromHours(1)); // --> Backup() should be called
// Assert
offlineHourlyDatabaseBackup.ExecuteTask.IsCompletedSuccessfully.Should().BeTrue();
mockOfflineDatabaseBackupService.Verify(x => x.Backup(), Times.AtLeast(2));
}
Here is my TestTimeProvider.cs
,
internal class TestTimeProvider(DateTimeOffset now) : TimeProvider
{
public override DateTimeOffset GetUtcNow() => now;
public override ITimer CreateTimer(TimerCallback callback, object? state, TimeSpan dueTime, TimeSpan period)
{
return base.CreateTimer(callback, state, TimeSpan.Zero, TimeSpan.FromHours(1));
}
public void Advance(TimeSpan timeSpan)
{
now = now.Add(timeSpan);
}
}
When I run the test I expect Callback
Backup
method to be called two times. One because of await offlineHourlyDatabaseBackup.ExecuteTask!;
and another time because of timeProvider.Advance(TimeSpan.FromHours(1));
but getting the exception message as follows
Expected invocation on the mock at least 2 times, but was 0 times: x => x.Backup()
Upon debugging I have noticed that before the first Backup()
call gets executed the assertion line gets completed and throws above error.
Please can you assist me on what I'm doing wrong here ?
Finally I solved the above challenge.
I came to know that there are test fakes available from Microsoft
as Nuget
Packages.
For TimerProvider
- Microsoft.Extensions.TimeProvider.Testing
And simply changed the test as shown below with FakeTimeProvider
[Fact]
public async Task OfflineHourlyDatabaseBackup_ExecuteAsync()
{
// Arrange
var serviceCollection = new ServiceCollection();
var mockOfflineDatabaseBackupService = new Mock<IOfflineDatabaseBackupService>();
serviceCollection.AddSingleton(_ => mockOfflineDatabaseBackupService.Object);
var now = DateTimeOffset.UtcNow;
var timeProvider = new FakeTimeProvider(); // --> Changed here
var serviceProvider = serviceCollection.BuildServiceProvider();
var logger = new FakeLogger<OfflineHourlyDatabaseBackup>();
var configuration = new Mock<IConfiguration>();
// Act
using var offlineHourlyDatabaseBackup = new OfflineHourlyDatabaseBackup(logger, timeProvider, serviceProvider);
await offlineHourlyDatabaseBackup.StartAsync(default);
await offlineHourlyDatabaseBackup.ExecuteTask!;
timeProvider.Advance(TimeSpan.FromHours(1));
// Assert
offlineHourlyDatabaseBackup.ExecuteTask.IsCompletedSuccessfully.Should().BeTrue();
mockOfflineDatabaseBackupService.Verify(x => x.Backup(), Times.AtLeast(2));
}
Source: https://devblogs.microsoft.com/dotnet/fake-it-til-you-make-it-to-production/