Subcutaneous Testing in ASP.NET Core
Posted on Thursday, 14th March 2019
Having successfully applied Subcutaneous testing to a number of projects I was interested to see what options were available in ASP.NET Core. In this post we’ll touch on what Subcutaneous testing is, its trade offs and how we can perform such tests in ASP.NET Core.
What is Subcutaneous Testing?
I first learnt about Subcutaneous testing from Matt Davies and Rob Moore’s excellent Microtesting - How We Set Fire To The Testing Pyramid While Ensuring Confidence talk at NDC Sydney. The talk introduces its viewers to various testing techniques and libraries you can use to ensure speed and confidence in your testing strategy whilst avoiding the crippling test brittleness that often ensues.
The term subcutaneous means “situated or applied under the skin”, which translated to an application would mean just under the UI layer.
Why would you want to test under the UI later?
As Martin Fowler highlights in his post on Subcutaneous testing, such a means of testing is especially useful when trying to perform functional tests where you want to exercise end-to-end behaviour whilst avoiding some of the difficulties associated with testing via the UI itself.
Martin goes on to say (emphasis is my own, which we’ll touch on later):
Subcutaneous testing can avoid difficulties with hard-to-test presentation technologies and usually is much faster than testing through the UI. The big danger is that, unless you are a firm follower of keeping all useful logic out of your UI, subcutaneous testing will leave important behaviour out of its test.
Let’s take a look at an example…
Subcutaneous testing an ASP.NET Core application
In previous non-core versions of ASP.NET MVC I used to use a tool called FluentMvcTesting, however with the advent of .NET Core I was keen to see what options were available to create subcutaneous tests whilst leaning on some of the primitives that exist in bootstrapping one’s application.
This investigation ultimately lead me to the solution we’ll discuss shortly via the new .AddControllersAsServices()
extension method that can be called via the IMvcBuilder
interface.
It’s always nice to understand what these APIs are doing so let’s take a moment to look under the covers of the AddControllersAsServices
method:
public static IMvcBuilder AddControllersAsServices(this IMvcBuilder builder)
{
var feature = new ControllerFeature();
builder.PartManager.PopulateFeature(feature);
foreach (Type type in feature.Controllers.Select(c => c.AsType()))
{
builder.Services.TryAddTransient(type, type);
}
builder.Services.Replace(
ServiceDescriptor.Transient<IControllerActivator, ServiceBasedControllerActivator>());
return builder;
}
Looking at the source code it appears that the AddControllersAsServices()
method populates the ControllerFeature.Controllers
collection with a list of controllers via the ControllerFeatureProvider
(which is indirectly invoked via the PopulateFeature
call). The ControllerFeatureProvider
then loops through all the “parts” (classes within your solution) looking for classes that are controllers. It does this, among a few other things such as checking to see if the type is public, by looking for anything ending in with the strong “Controller”.
Once the controllers in your application are added to the collection within the ControllerFeature.Controllers
collection, they’re then registered as transient services within .NET Core’s IOC container (IServiceCollection
).
What does this mean for us and Subcutaneous testing? Ultimately this means we can resolve our chosen controller from the IOC container and in doing so it will resolve any dependencies also registered in the controller, such as services, repositories etc.
Putting it together
First we’ll need to call the AddControllersAsServices()
extension method as part of the AddMvc
method chains in Startup.cs
:
// Startup.cs
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
...
services.AddMvc().AddNewtonsoftJson().AddControllersAsServices();
}
}
Alternatively if you’ve no reason to resolve controllers directly from your IOC container other than for testing you might prefer to configure it as part of your test infrastructure. A common pattern to do this is to move the MVC builder related method calls into a virtual method so we can override it and call the base method like so:
// Startup.cs
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
...
ConfigureMvcBuilder(services);
}
public virtual IMvcBuilder ConfigureMvcBuilder(IServiceCollection services)
=> services.AddMvc().AddNewtonsoftJson();
}
Now all that’s left to do is create a derived version of Startup.cs
which we’ll call TestStartup
then call AddControllersAsServices()
after setting up MVC.
// TestStartup.cs
public class TestStartup : Startup
{
public TestStartup(IConfiguration configuration) : base(configuration)
{
}
public override IMvcBuilder ConfigureMvcBuilder(IServiceCollection serviceCollection)
{
var services = base.ConfigureMvcBuilder(serviceCollection);
return services.AddControllersAsServices();
}
}
We can now start to resolve controllers from the container using the .Host.Services.GetService<T>()
method. Putting it in a simple test fixture would look like this:
// SubcutaneousTestFixture.cs
public class SubcutaneousTestFixture
{
private TestServer _server;
public T ResolveController<T>() where T : Controller
=> _server.Host.Services.GetService<T>();
public SubcutaneousTestFixture Run<T>() where T : Startup
{
var webHostBuilder = WebHost.CreateDefaultBuilder();
webHostBuilder.UseStartup<T>();
webHostBuilder.UseContentRoot(Directory.GetCurrentDirectory());
_server = new TestServer(webHostBuilder);
return this;
}
}
Which can be invoked like so within our test:
// NewsDeletionTests.cs
public class NewsDeletionTests : IClassFixture<SubcutaneousTestFixture>
{
private readonly SubcutaneousTestFixture _fixture;
public NewsDeletionTests(SubcutaneousTestFixture fixture)
{
_fixture = fixture.Run<TestStartup>();
}
...
}
Now if we want to test our controller actions (using a trivial example here) we now do so without having to go through too much effort associated with end to end testing such as using Selenium:
//DeleteNewsController.cs
[HttpPost]
public async Task<IActionResult> Delete(DeleteNews.Command command)
{
try
{
await _mediator.Send(command);
}
catch (ValidationException e)
{
return View(new ManageNewsViewModel
{
Errors = e.Errors.ToList()
});
}
return RedirectToRoute(RouteNames.NewsManage);
}
// DeleteNewsTests.cs
[Fact]
public void UserIsRedirectedAfterSuccessfulDeletion()
{
// Arrange
var controller = _fixture.ResolveController<DeleteNewsController>();
// Act
var result = (RedirectToActionResult) controller.Delete(new DeleteNews.Command { Id = 1 });
// Assert
result.ControllerName.ShouldBe(nameof(NewsController));
result.ActionName.ShouldBe(nameof(NewsController.Index));
}
[Fact]
public void ErrorsAreReturnedToUser()
{
// Arrange
var controller = _fixture.ResolveController<DeleteNewsController>();
// Act
var result = (ViewResult) controller.Delete(new DeleteNews.Command { Id = 1 });
var viewModel = result.Model as DeleteNewsViewModel;
// Assert
viewModel?.Errors.Count.ShouldBe(3);
}
At this point we’ve managed to successfully exercise end-to-end behaviour in our application whilst avoiding the difficulties often associated with with hard-to-test UI technologies. At the same time we’ve successfully avoided mocking out dependencies so our tests won’t start breaking when we modify the implementation details.
There are no silver bullets
Testing is an imperfect size and there’s rarely a one size fits all solution. As Fred Brooks put it, “there are no silver bullets”, and this approach is no exception - it has its limitations which, depending on how you architect your application, could affect its viability. Let’s see what they are:
No HTTP Requests
As you may have noticed, there’s no HTTP request, which means anything regarding HttpContextRequest filters No HTTP request also means any global filters you may have will not be invoked.
ModelState
is not set
As we’re not generating an HTTP request, you’ll noticeModelState
validation is not set and would require mocking. To some this may or may not be a problem. Personally as someone that’s a fan of MediatR or Magneto coupled with FluentValidation, my validation gets pushed down into my domain layer. Doing this also means I don’t have to lean on validating my input models using yucky attributes.
Conclusion
Hopefully this post has given you a brief insight as to what Subcutaneous Testing is, the trade offs one has to make and an approach you could potentially use to test your applications in a similar a manner. There are a few approaches out there, but on occasion there are times where I wish to test a behaviour inside of my controller so this can do the trick.
Ultimately Subcutaneous Testing will enable you to test large parts of your application but will still leave you lacking the confidence you’d require in order to push code into production, this is where you could fill in the gaps with tests that exercise the UI.
Enjoy this post? Don't be a stranger!
Follow me on Twitter at @_josephwoodward and say Hi! I love to learn in the open, meet others in the community and talk Go, software engineering and distributed systems related topics.