Last active
September 30, 2017 21:25
-
-
Save rodolfograve/16e603a86565536063932e03fa4afc10 to your computer and use it in GitHub Desktop.
Testing ASP.NET Core services
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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(); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
[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)) | |
); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Adds the IServiceProvider property we need for our testing strategy to work | |
public interface ITestableStartup : IStartup | |
{ | |
IServiceProvider ServiceProvider { get; } | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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(); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/// <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 | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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(); | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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)); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/// <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