Skip to content

Commit

Permalink
feat: Support for background services [ci]
Browse files Browse the repository at this point in the history
  • Loading branch information
bmazzarol committed Sep 29, 2022
1 parent 78f8b3d commit a902641
Show file tree
Hide file tree
Showing 13 changed files with 532 additions and 0 deletions.
77 changes: 77 additions & 0 deletions BunsenBurner.Background.Tests/AaaTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 1,77 @@
using System.Diagnostics.CodeAnalysis;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;

namespace BunsenBurner.Background.Tests;

using static Aaa;
using static BunsenBurner.Aaa;

internal sealed class Background : BackgroundService
{
private readonly ILogger<Background> _logger;

public Background(ILogger<Background> logger) => _logger = logger;

[ExcludeFromCodeCoverage]
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
_logger.LogInformation("Starting work...");
while (!stoppingToken.IsCancellationRequested)
{
var delay = TimeSpan.FromMilliseconds(10);
_logger.LogInformation("Doing work for {Delay} duration", delay);
await Task.Delay(delay, stoppingToken);
_logger.LogInformation("Work complete");
}
}
}

internal sealed class Startup
{
public void ConfigureServices(IServiceCollection services)
{
services.AddLogging();
services.AddHostedService<Background>();
}
}

public static class AaaTests
{
[Fact(DisplayName = "A background service can be started and run for a period")]
public static async Task Case1() =>
await ArrangeBackgroundService<Startup, Background>()
.ActAndRunFor(TimeSpan.FromMilliseconds(40))
.Assert(store =>
{
Assert.NotEmpty(store);
Assert.Contains(store, x => x.Message == "Work complete");
});

[Fact(
DisplayName = "A background service can be started and run for a period with a description"
)]
public static async Task Case2() =>
await "Some description"
.ArrangeBackgroundService<Startup, Background>()
.ActAndRunFor(TimeSpan.FromMilliseconds(40))
.Assert(store =>
{
Assert.NotEmpty(store);
Assert.Contains(store, x => x.Message == "Work complete");
});

[Fact(
DisplayName = "A background service can be started and run for a period with existing arranged data"
)]
public static async Task Case3() =>
await Arrange(() => 1)
.AndABackgroundService<int, Startup, Background>()
.ActAndRunFor(x => x.BackgroundServiceContext, TimeSpan.FromMilliseconds(40))
.Assert(store =>
{
Assert.NotEmpty(store);
Assert.Contains(store, x => x.Message == "Work complete");
});
}
43 changes: 43 additions & 0 deletions BunsenBurner.Background.Tests/BddTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 1,43 @@
namespace BunsenBurner.Background.Tests;

using static Bdd;
using static BunsenBurner.Bdd;

public static class BddTests
{
[Fact(DisplayName = "A background service can be started and run for a period")]
public static async Task Case1() =>
await GivenABackgroundService<Startup, Background>()
.WhenRunFor(TimeSpan.FromMilliseconds(40))
.Then(store =>
{
Assert.NotEmpty(store);
Assert.Contains(store, x => x.Message == "Work complete");
});

[Fact(
DisplayName = "A background service can be started and run for a period with a description"
)]
public static async Task Case2() =>
await "Some description"
.GivenABackgroundService<Startup, Background>()
.WhenRunFor(TimeSpan.FromMilliseconds(40))
.Then(store =>
{
Assert.NotEmpty(store);
Assert.Contains(store, x => x.Message == "Work complete");
});

[Fact(
DisplayName = "A background service can be started and run for a period with existing arranged data"
)]
public static async Task Case3() =>
await Given(() => 1)
.AndABackgroundService<int, Startup, Background>()
.WhenRunFor(x => x.BackgroundServiceContext, TimeSpan.FromMilliseconds(40))
.Then(store =>
{
Assert.NotEmpty(store);
Assert.Contains(store, x => x.Message == "Work complete");
});
}
28 changes: 28 additions & 0 deletions BunsenBurner.Background.Tests/BunsenBurner.Background.Tests.csproj
Original file line number Diff line number Diff line change
@@ -0,0 1,28 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<IsPackable>false</IsPackable>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.1.0" />
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.3">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="coverlet.collector" Version="3.1.2">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\BunsenBurner.Background\BunsenBurner.Background.csproj" />
</ItemGroup>

</Project>
1 change: 1 addition & 0 deletions BunsenBurner.Background.Tests/GlobalUsings.cs
Original file line number Diff line number Diff line change
@@ -0,0 1 @@
global using Xunit;
93 changes: 93 additions & 0 deletions BunsenBurner.Background/Aaa.cs
Original file line number Diff line number Diff line change
@@ -0,0 1,93 @@
using BunsenBurner.Logging;
using Microsoft.Extensions.Hosting;

namespace BunsenBurner.Background;

using AaaScenario = Scenario<Syntax.Aaa>;

/// <summary>
/// DSL for building tests using an arrange, act, assert syntax
/// </summary>
public static class Aaa
{
/// <summary>
/// Arranges a background service context to run
/// </summary>
/// <typeparam name="TStartup">startup class</typeparam>
/// <typeparam name="TBackgroundService">background service class</typeparam>
/// <returns>arranged scenario</returns>
[Pure]
public static AaaScenario.Arranged<
BackgroundServiceContext<TBackgroundService>
> ArrangeBackgroundService<TStartup, TBackgroundService>()
where TBackgroundService : IHostedService
where TStartup : new() =>
Shared.ArrangeBackgroundService<TStartup, TBackgroundService, Syntax.Aaa>();

/// <summary>
/// Arranges a background service context to run
/// </summary>
/// <param name="name">name/description</param>
/// <typeparam name="TStartup">startup class</typeparam>
/// <typeparam name="TBackgroundService">background service class</typeparam>
/// <returns>arranged scenario</returns>
[Pure]
public static AaaScenario.Arranged<
BackgroundServiceContext<TBackgroundService>
> ArrangeBackgroundService<TStartup, TBackgroundService>(this string name)
where TBackgroundService : IHostedService
where TStartup : new() =>
name.ArrangeBackgroundService<TStartup, TBackgroundService, Syntax.Aaa>();

/// <summary>
/// Arranges a background service context to run along with existing arranged data
/// </summary>
/// <param name="scenario">scenario</param>
/// <typeparam name="TData">current arranged data</typeparam>
/// <typeparam name="TStartup">startup class</typeparam>
/// <typeparam name="TBackgroundService">background service class</typeparam>
/// <returns>arranged scenario</returns>
[Pure]
public static AaaScenario.Arranged<(TData Data, BackgroundServiceContext<TBackgroundService> BackgroundServiceContext)> AndABackgroundService<
TData,
TStartup,
TBackgroundService
>(this AaaScenario.Arranged<TData> scenario)
where TBackgroundService : IHostedService
where TStartup : new() =>
scenario.AndABackgroundService<TData, TStartup, TBackgroundService, Syntax.Aaa>();

/// <summary>
/// Runs the background service for the given time, returning any log messages
/// </summary>
/// <param name="scenario">arranged scenario</param>
/// <param name="fn">function to setup and return the background service and store</param>
/// <param name="runDuration">duration the background service should run for</param>
/// <typeparam name="TData">arranged data</typeparam>
/// <typeparam name="TBackgroundService">background service to test</typeparam>
/// <returns>acted scenario</returns>
[Pure]
public static AaaScenario.Acted<TData, LogMessageStore> ActAndRunFor<TData, TBackgroundService>(
this AaaScenario.Arranged<TData> scenario,
Func<TData, BackgroundServiceContext<TBackgroundService>> fn,
TimeSpan runDuration
) where TBackgroundService : IHostedService =>
scenario.ActAndRunFor<TData, TBackgroundService, Syntax.Aaa>(fn, runDuration);

/// <summary>
/// Runs the background service for the given time, returning any log messages
/// </summary>
/// <param name="scenario">arranged scenario</param>
/// <param name="runDuration">duration the background service should run for</param>
/// <typeparam name="TBackgroundService">background service to test</typeparam>
/// <returns>acted scenario</returns>
[Pure]
public static AaaScenario.Acted<
BackgroundServiceContext<TBackgroundService>,
LogMessageStore
> ActAndRunFor<TBackgroundService>(
this AaaScenario.Arranged<BackgroundServiceContext<TBackgroundService>> scenario,
TimeSpan runDuration
) where TBackgroundService : IHostedService =>
scenario.ActAndRunFor<TBackgroundService, Syntax.Aaa>(runDuration);
}
56 changes: 56 additions & 0 deletions BunsenBurner.Background/BackgroundServiceBuilder.cs
Original file line number Diff line number Diff line change
@@ -0,0 1,56 @@
using System.Collections.Concurrent;
using BunsenBurner.Logging;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;

namespace BunsenBurner.Background;

/// <summary>
/// Provides a cache/builder for background services
/// </summary>
public static class BackgroundServiceBuilder
{
private static ConcurrentDictionary<string, Lazy<IServiceProvider>> ServiceProviderCache =>
new();

/// <summary>
/// Creates a new instance of the background service and log message store from the provided startup class
/// </summary>
/// <typeparam name="TStartup">startup class</typeparam>
/// <typeparam name="TBackgroundService">background service</typeparam>
/// <returns>instance of the background service and log message store</returns>
public static BackgroundServiceContext<TBackgroundService> Create<
TStartup,
TBackgroundService
>()
where TStartup : new()
where TBackgroundService : IHostedService
{
var startupType = typeof(TStartup);
var sp = ServiceProviderCache
.GetOrAdd(
startupType.FullName ?? startupType.Name,
static _ =>
new Lazy<IServiceProvider>(() =>
{
var store = LogMessageStore.New();
var configureServicesMethod = typeof(TStartup).GetMethod(
"ConfigureServices"
);
var sc = new ServiceCollection();
var startup = new TStartup();
configureServicesMethod?.Invoke(startup, new object[] { sc });
sc.ClearLoggingProviders().AddDummyLogger(store).AddSingleton(store);
return sc.BuildServiceProvider();
})
)
.Value;
var services = sp.GetRequiredService<IEnumerable<IHostedService>>();
var store = sp.GetRequiredService<LogMessageStore>();
store.Clear();
return new BackgroundServiceContext<TBackgroundService>(
services.OfType<TBackgroundService>().First(),
store
);
}
}
15 changes: 15 additions & 0 deletions BunsenBurner.Background/BackgroundServiceContext.cs
Original file line number Diff line number Diff line change
@@ -0,0 1,15 @@
using BunsenBurner.Logging;
using Microsoft.Extensions.Hosting;

namespace BunsenBurner.Background;

/// <summary>
/// Context used to test a background service
/// </summary>
/// <param name="Service">background service</param>
/// <param name="Store">log message store</param>
/// <typeparam name="TBackgroundService">background service type</typeparam>
public sealed record BackgroundServiceContext<TBackgroundService>(
TBackgroundService Service,
LogMessageStore Store
) where TBackgroundService : IHostedService;
Loading

0 comments on commit a902641

Please sign in to comment.