I have an ASP.NET Core web API application that's set up as an AWS Serverless lambda function with API Gateway.
I'm working with an instance of APIGatewayProxyFunction
and I'm trying to unit test some of my controller behavior by injecting NSubstitute
versions of my database repo.
Now, of course I can instantiate my controller directly and inject the mocked dependencies, but using the auto-gen'd LambdaEntryPoint
class and taking advantage of the host builder logic lets me get all the MVC routing goodies and I can test actual HTTP method matching and route matching.
My problem is, builder.UseStartup<Startup>()
uses my real DI service registration code. I was hoping I could override this behavior and register a mocked instance of my database repo class.
There doesn't seem to be a way to get at the ServiceCollection in the unit test once the object is constructed, so I thought I'd just sub-class the LambdaEntryPoint
and override the Init()
function to supply a subclass of Startup
MockStartup
instead which registers mocked instances.
The problem it seems is that Init()
actually gets called during the constructor chain, so I can't really feed mocked instances into my subclass to be used during the Init()
override.
Super pared down example:
var myClass = new ChildClass(7);
public class BaseClass
{
public BaseClass()
{
Console.WriteLine("Base class constructing");
Init();
}
public virtual void Init()
{
Console.WriteLine("Base class init");
}
}
public class ChildClass : BaseClass
{
private readonly int? _mockedService;
public ChildClass(int mockedService)
{
_mockedService = mockedService;
Console.WriteLine("Child class constructed");
}
public override void Init()
{
Console.WriteLine($"Child class init with mocked service {_mockedService}");
}
}
This, of course does not work, because _mockedService
is still null when we get to executing the overridden Init()
function.
So, I'm looking for guidance on how I can write a unit test which can submit actual JSON posts to prove MVC routes and HTTP methods for my application while still providing a mocked instance of my database interface?
I'm open to all options, but if possible, I'd like to do this without spinning up a full http webservice and actually submitting http requests, but if that's the only option, guidance on the best way to do that with substitutes would be appreciated as well.
Thanks.
CodePudding user response:
I was able to solve this using a static Dictionary and GetHashCode()
to uniquely identify each LambdaEntryPoint
object created by different tests.
public class TestEntryPoint : LambdaEntryPoint
{
private static readonly Dictionary<int, IDbRepository> Repos = new();
public IDbRepository Repository => Repos[GetHashCode()];
protected override void Init(IWebHostBuilder builder)
{
builder.UseStartup(context =>
{
var repo = Substitute.For<IDbRepository>();
Repos[GetHashCode()] = repo;
var startup = new MockStartup(context.Configuration, repo);
return startup;
});
}
}
public class MockStartup : Startup
{
private readonly IDbRepository _repository;
public MockStartup(IConfiguration configuration, IDbRepository repository) : base(configuration)
{
_repository = repository;
}
public override void RegisterServices(IServiceCollection services)
{
services
.AddTransient<IServiceConfiguration, LambdaServiceConfiguration>()
.AddTransient(_ => _repository);
}
}
This allows my tests to do:
var lambdaFunction = new TestEntryPoint();
lambdaFunction.Repository.Whatever(...).Returns(...);
using lambdaFunction.Repository
as my mock