Skip to content

Instantly share code, notes, and snippets.

@rodolfograve
Last active September 30, 2017 21:25
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save rodolfograve/16e603a86565536063932e03fa4afc10 to your computer and use it in GitHub Desktop.
Save rodolfograve/16e603a86565536063932e03fa4afc10 to your computer and use it in GitHub Desktop.
Testing ASP.NET Core services
public async Task WhenXThenY(
IRepository<SomeEntity> repository, // For this to work, a TestServer must have been started and
// this IRepository must be the same instance that the Controller is going to use when processing the request
WebApiClient client, // This must be wrapping an HttpClient connected to the TestServer
ISomeExternalServiceGateway gateway // Must also be the same instance to be used by the Controller)
{
var entityInRequiredState = CreateEntityInStateX();
repository.GetById(entityInRequiredState.Id).Return(entityInRequiredState);
await client.Post($"controller/action/{entityInRequiredState.Id}");
entityInRequiredState.ShouldBeInStateY();
}
public class Program
{
public static void Main(string[] args)
{
var host = new WebHostBuilder()
.UseKestrel()
.UseContentRoot(Directory.GetCurrentDirectory())
.UseIISIntegration()
.UseStartup<Startup>() // This is the key line here
.Build();
host.Run();
}
}
// And the final piece of the puzzle, which is a fairly generic SpecimenBuilder that delegates all requests to an IServiceProvider
/// <summary>
/// Resolves all requests from the provided <see cref="IServiceProvider"/>
/// </summary>
public class DependenciesSpecimenBuilder : ISpecimenBuilder
{
private readonly IServiceProvider serviceProvider;
public DependenciesSpecimenBuilder(IServiceProvider serviceProvider)
{
this.serviceProvider = serviceProvider ?? throw new ArgumentNullException(nameof(serviceProvider));
}
public object Create(object request, ISpecimenContext context)
{
var type = request as Type;
if (type == null) return new NoSpecimen();
var result = serviceProvider.GetService(type);
// serviceProvider seems to be resolving any IList requests with an empty one, which is not what we want.
if ((result as Array)?.Length == 0) return new NoSpecimen();
return result ?? new NoSpecimen();
}
}
[AttributeUsage(AttributeTargets.Method, AllowMultiple = false)]
public class IntegrationTestAutoDataAttribute : AutoDataAttribute
{
public IntegrationTestAutoDataAttribute()
{
Fixture.Customize(new WebApiCustomization(
new Startup( // This is the same Startup class used in Program.cs
// But here we are providing mocks for IRepository<SomeEntity>, etc.
IntegrationTestsDependencies.AddDependencies))
);
}
}
// Adds the IServiceProvider property we need for our testing strategy to work
public interface ITestableStartup : IStartup
{
IServiceProvider ServiceProvider { get; }
}
public class Program
{
public static void Main(string[] args)
{
var startup = new Startup(ProductionDependencies.AddDependencies);
var host = new WebHostBuilder()
.UseKestrel()
.UseContentRoot(Directory.GetCurrentDirectory())
.UseIISIntegration()
// This behaviour is not very well documented, but it's there and it's a supported way to provide a IStartup:
// https://www.strathweb.com/2017/06/resolving-asp-net-core-startup-class-from-the-di-container/
.ConfigureServices(services => services.AddSingleton(startup))
.Build();
host.Run();
}
}
// Must inherit form IStartup in order to get the desired behaviour out of the WebHostBuilder
// ITestableStartup is just adding the IServiceProvider property we need for our testing strategy to work
public class Startup : ITestableStartup
{
private readonly Func<IServiceCollection, IConfigurationRoot, IServiceCollection> configureServices;
public Startup(Func<IServiceCollection, IConfigurationRoot, IServiceCollection> configureServices)
{
this.configureServices = configureServices;
}
public IServiceProvider ServiceProvider { get; private set; }
public IServiceProvider ConfigureServices(IServiceCollection services)
{
services
.AddOptions()
.AddControllersAsServices()
// All your typical config here, including everything that you don't want to mock...
;
// This call lets you change your dependencies dependending on which flavour of the service you're running (Production, Integration tests, End to end tests, etc.)
configureServices(services);
// Save a reference to the ServiceProvider. Notice this is the same IServiceProvider that will be used to create Controllers, etc.
return ServiceProvider = services.BuildServiceProvider();
}
}
/// <summary>
/// An HttpClient connected to a single TestServer. See <seealso cref="WebApiServerFixture" />
/// </summary>
public class WebApiClientFixture : IDisposable
{
public WebApiClientFixture(HttpClient httpClient)
{
HttpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient));
}
protected readonly HttpClient HttpClient;
public HttpResponseFixture LatestResponse { get; private set; }
public async Task<HttpResponseFixture> Get(string requestUri) =>
LatestResponse = new HttpResponseFixture(await HttpClient.GetAsync(requestUri).ConfigureAwait(false));
public async Task<HttpResponseFixture> Post<TContent>(string requestUri, TContent content) =>
LatestResponse = new HttpResponseFixture(await HttpClient.PostAsync(requestUri, CreateHttpContent(content)).ConfigureAwait(false));
public async Task<HttpResponseFixture> Put<TContent>(string requestUri, TContent content) =>
LatestResponse = new HttpResponseFixture(await HttpClient.PutAsync(requestUri, CreateHttpContent(content)).ConfigureAwait(false));
// Any other convenience methods you need for your testing
}
// AutoFixture-specific implementation so that a WebClientFixture is created from the TestServer
public class WebClientFixtureSpecimenBuilder : ISpecimenBuilder
{
public object Create(object request, ISpecimenContext context)
{
if (context == null) throw new ArgumentNullException(nameof(context));
var type = request as Type;
if (type == null || type != typeof(WebClientFixture))
{
return new NoSpecimen();
}
else
{
// Resolve the singletong WebServerFixture from the AutoFixture context
var singletonServer = ((WebServerFixture)context.Resolve(typeof(WebServerFixture)));
return singletonServer.CreateClient();
}
}
}
// AutoFixture-specific implementation of the "magic" that lets you create a TestServer and use its IServiceProvider
// to resolve the parameters of a test method
public class WebCustomization : ICustomization
{
public WebCustomization(ITestableStartup startup)
{
this.startup = startup;
}
private readonly ITestableStartup startup;
public void Customize(IFixture fixture)
{
var singletonServerFixture = new WebServerFixture(startup);
// Register the server as a singleton in the AutoFixture scope (fixture).
// All requests to resolve a WebServerFixture will return this same instance
fixture.Inject(singletonServerFixture);
if (startup.ServiceProvider == null)
throw new Exception($"{nameof(startup)}.{nameof(startup.ServiceProvider)} is still null after creating the {nameof(TestServer)}. Make sure your {nameof(ITestableStartup)} saves a reference to the {nameof(IServiceProvider)}.");
fixture.Customizations.Add(new WebClientFixtureSpecimenBuilder());
fixture.Customizations.Add(new DependenciesSpecimenBuilder(startup.ServiceProvider));
}
}
/// <summary>
/// Creates a TestServer using the provided IStartup instance.
/// Having access to the IStartup instance (rather than letting WebHostBuilder create its own)
/// gives us more control of the dependencies, and facilitates testing.
/// You'll usually take a dependency on <seealso cref="WebClientFixture"/> to interact with the server.
/// </summary>
public class WebServerFixture : IDisposable
{
public WebServerFixture(IStartup startup)
{
if (startup == null)
throw new ArgumentNullException(nameof(startup));
Server = new TestServer(
new WebHostBuilder()
.ConfigureServices(x => x.AddSingleton(startup))
// Workaround ASP.NET Core bug: https://github.com/aspnet/Hosting/issues/1137
.UseSetting(WebHostDefaults.ApplicationKey, startup.GetType().Assembly.FullName));
}
private readonly TestServer Server;
public WebApiClientFixture CreateClient() => new WebApiClientFixture(Server.CreateClient());
public void Dispose() => Server?.Dispose();
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment