How to fake declared services in Startup.cs during testing? - c#

I would like to write integration tests for my Asp .net core application, but I don't want my tests to use real implemetation of some services.
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
...
services.AddTransient<IExternalService,ExternalService>();
...
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
...
}
}
public interface IExternalService
{
bool Verify(int id);
}
public class ExternalService : IExternalService
{
public bool Verify(int id)
{
//Implemetation is here.
//I want to fake this implemetation during testing.
}
}
[Fact]
public void TestCase()
{
//Stub out service
var myExtService = new Mock<IExternalService>();
//Setup response by stub
myExtService
.Setup(p => p.Verify(It.IsAny<int>()))
.Returns(false);
var host = new WebHostBuilder()
.UseStartup<Startup>()
.ConfigureServices((services) =>
{
//Setup injection
services.AddTransient<IExternalService>((a) =>
{
return myExtService.Object;
});
});
var server = new TestServer(host);
var client = server.CreateClient();
var response = client.GetAsync("/").Result;
var responseString = response.Content.ReadAsStringAsync().Result;
Assert.Contains("Your service returned: False", responseString);
}
Current injection setup in test case does not work, because ExternalService is injected over the mock.
However the test will pass when I remove services.AddTransient<IExternalService,ExternalService>; from Startup.
Most likely the one in Startup is called later and all the setup in that class is preferred by application.
What options do I have to setup some dependecies in tests, but use everything else as they are declared in Startup?
UPDATE
Application code should be unaware of tests.
Tests should be aware of:
(weakly typed) Endpoint - if this changes then test should fail
IExternalService interface
Tests should not care if application has razor pages or uses mvc or how the application is wired between endpoint and IExternalService.
Tests should not have to setup or configure application (apart from stubbing IExternalService) in order to make it work.
I understand that WebHostBuilder still has to be created, but my point is that configuration should be bare minimum in test case and majority of configuration should still be described on application side.

The only option I know of is to setup WebHostBuilder with UseEnvironment:
var host = new WebHostBuilder()
.UseStartup<Startup>()
.ConfigureServices(services =>
{
//Setup injection
services.AddTransient<IExternalService>(provider =>
{
return myExtService.Object;
});
})
.UseEnvironment("IntegrationTest");
And then add a condition in the ConfigureServices method in the Startup:
public void ConfigureServices(IServiceCollection services)
{
if (Configuration["Environment"] != "IntegrationTest")
{
services.AddTransient<IExternalService, ExternalService>();
}
services.AddMvc();
// ...
}
UPDATE:
I did some more poking around and another option is to not use UseStartup extension method but rather configure the WebHostBuilder directly. You can do this a number of ways but I thought that you could possibly create your own extension method to create a template in your tests:
public static class WebHostBuilderExt
{
public static WebHostBuilder ConfigureServicesTest(this WebHostBuilder #this, Action<IServiceCollection> configureServices)
{
#this.ConfigureServices(services =>
{
configureServices(services);
services.AddMvc();
})
.Configure(builder =>
{
builder.UseMvc();
});
return #this;
}
}
Now your tests can be setup like the following:
var host = new WebHostBuilder()
.ConfigureServicesTest(services =>
{
//Setup injection
services.AddTransient<IInternalService>(provider =>
{
return myExtService.Object;
});
});
var server = new TestServer(host);
This means that you will have to explicitly setup all the implementations that the container will resolve for the specific endpoint you are calling. You can choose to mock or use the the concrete implementations.

The only thing yo need to change is to use ConfigureTestServices instead of ConfigureServices. ConfigureTestServices runs after your Startup, therefor you can override real implementations with mocks/stubs. ConfigureServices was newer intended for that purpose, rather, it configures "host services", which are used during the host-building phase of the application, and copied into the application's DI container.
ConfigureTestServices is available in ASP Core version 2.1 and higher.
var host = new WebHostBuilder()
.UseStartup<Startup>()
.ConfigureTestServices((services) =>
{
//Setup injection
services.AddTransient<IExternalService>((a) =>
{
return myExtService.Object;
});
});

So after hours of research I found a solution.
I could not find a way to solely use built-in dependency injection solution, so I opted to choose 3rd party DI solution - Autofac
Idea is to use WebHostBuilder (declared Main Program) and add necessary options so I can fake some services during testing.
Something that I learned:
If you use Startup as host.UseStartup<Startup> it will be created after host.ConfigureServices()
You cannot inject something to Startup like host.UseStartup<Startup>(new Dependency())
However if you have registred dependency in host.ConfigureServices(services => services.AddTransient<IDependency, MyDependency>()), then it will be resolved before Startup is created and constructor public Startup(IDependency dependency) is used to create Startup.
My application side:
public class Program
{
public static void Main(string[] args)
{
CreateWebHost(args)
.Build()
.Run();
}
public static IWebHostBuilder CreateWebHost(string[] args) =>
WebHost.CreateDefaultBuilder(args)
.ConfigureServices((services) =>
{
//Setup autofac.
services.AddAutofac();
//Register module dependency that Startup requires.
services.AddTransient<Module, MyAutofacModule>();
////It would a bit cleaner to use autofac to setup Startup dependency,
////but dependency did not get resolved for Startup.
//services.AddAutofac((builder) =>
//{
// builder.RegisterModule(new AutofacModule());
//});
})
.UseStartup<Startup>();
}
public class MyAutofacModule : Module
{
protected override void Load(ContainerBuilder builder)
{
//Register all application dependencies in this module.
builder.Register((c) => new ExternalService()).As<IExternalService>();
}
}
public class Startup
{
private Module applicationDIModule;
public Startup(Module applicationDIModule)
{
this.applicationDIModule = applicationDIModule;
}
public void ConfigureServices(IServiceCollection services)
{
//We can add build-in services such as mvc and authorization,
//but I would not use Add(Transient/Scoped/Singleton) here.
//You should register domain specific dependecies in MyAutofacModule,
//since it will be added after this method call.
services.AddMvc();
}
//This method is called after ConfigureServices (refer to Autofac link).
public void ConfigureContainer(ContainerBuilder builder)
{
//We will register injected module.
builder.RegisterModule(applicationDIModule);
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseMvcWithDefaultRoute();
}
}
Test case:
public class IntegrationTests
{
[Fact]
public void TestCase()
{
//Create and setup moq object as usual.
var service = new Mock<IExternalService>();
service
.Setup(p => p.Verify(It.IsAny<int>()))
.Returns(false);
//Bundle moq objects together for registration.
var attachFakes = new Action<ContainerBuilder>((builder) =>
{
builder.Register(c => service.Object);
});
//Use host builder that application uses.
var host = Program.CreateWebHost(new string[] { })
.UseContentRoot(GetContentRoot()) //Adjust content root since testproject.csproj is not in same folder as application.csproj
.ConfigureServices((services) =>
{
//We re-configure Module registration,
//so Startup is injected with our TestModule.
services.AddTransient<Module>((a) =>
{
return new TestModule(attachFakes);
});
});
//Create server to use our host and continue to test.
var server = new TestServer(host);
var client = server.CreateClient();
var response = client.GetAsync("/").Result;
var responseString = response.Content.ReadAsStringAsync().Result;
Assert.Contains("External service result: False", responseString);
}
private static string GetContentRoot()
{
var current = Directory.GetCurrentDirectory();
var parent = Directory.GetParent(current).Parent.Parent.Parent;
return Path.Combine(parent.FullName, "src");
}
}
public class TestModule : MyAutofacModule
{
private Action<ContainerBuilder> attachFakes;
public TestModule(Action<ContainerBuilder> attachFakes)
{
this.attachFakes = attachFakes;
}
protected override void Load(ContainerBuilder builder)
{
//We register everything in MyAutoFacModule before adding our fakes.
base.Load(builder);
//We add fakes and everything that is re-registered here will be used instead.
attachFakes.Invoke(builder);
}
}
Although it feels a bit brittle, but I still prefer this solution over what #ODawg suggested. His solution would work, but I see it would cause troubles in future when new test cases are added.

Related

ASP .Net Core get all classes that Implement interface and call their methods

I have an asp.net core 3.1 web application
I have an Interface which is implemented by 3 classes to configure database mapping. I want to call the method automatically during application configuration setup.
Following is my interface and their implementation.
public interface IMongoMapper
{
void Configure();
}
class TenantMap : IMongoMapper
{
public void Configure()
{
BsonClassMap.RegisterClassMap<Entities.Tenant>(cm =>
{
cm.AutoMap();
});
}
}
class CourseMap : IMongoMapper
{
public void Configure()
{
BsonClassMap.RegisterClassMap<Course>(cm =>
{
cm.AutoMap();
});
}
}
How to get all the classes that implement interface and call Configure
method appropriately?
You can use scope.ServiceProvider.GetServices<IMongoMapper>(); to get all classes that implement IMongoMapper interface.
You can use an extension method and call it in Configure method in startup class.
public static void IntializeMapping(this IApplicationBuilder app)
{
using (var scope = app.ApplicationServices.GetRequiredService<IServiceScopeFactory>().CreateScope())
{
var mappers = scope.ServiceProvider.GetServices<IMongoMapper>();
foreach (var map in mappers)
{
map.Configure();
}
}
}
and use it in startup class
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
app.IntializeMapping();
}
Update
According to Microsoft documentation better way is use this
public static async Task Main(string[] args)
{
var host = CreateHostBuilder(args).Build();
using (var scope = host.Services.CreateScope())
{
try
{
var mappers = scope.ServiceProvider.GetServices<IMongoMapper>();
foreach (var map in mappers)
{
map.Configure();
}
}
catch (Exception ex)
{
var logger = service.GetService<ILogger<Program>>();
logger.LogError(ex, "An error occurred mapping");
}
}
await host.RunAsync();
}
public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder.UseStartup<Startup>();
});
Microsoft Documentation
In older tutorials, you may see similar code in the Configure method in Startup.cs. We recommend that you use the Configure method only to set up the request pipeline. Application startup code belongs in the Main method.
Now the first time you run the application, the database will be created and seeded with test data. Whenever you change your data model, you can delete the database, update your seed method, and start afresh with a new database the same way. In later tutorials, you'll see how to modify the database when the data model changes, without deleting and re-creating it.
Assuming that you have empty constructor for the derived classes as mentioned in your example,you can do the below code you can get the interface by reflection and check which type can be assignable and !c.IsInterface so as it doesn't return the interface itself:
var result = typeof("Any Class").Assembly.GetTypes().Where(c => typeof(IMongoMapper).IsAssignableFrom(c) && !c.IsInterface);
foreach (var i in result)
{
((IMongoMapper)Activator.CreateInstance(i)).Configure();
}
You can do it via Reflection.
I just tried the code below and it works in dotnet core 3.1.
It works with the interface and the implementation class:
in the same assembly
in separate assemblies
var asm = Assembly.GetAssembly(typeof(YourClass));
var mapperTypes = asm.GetTypes().Where(x => x.GetInterface(nameof(IMongoMapper)) != null);
foreach(var mapperType in mapperTypes)
{
var mapper = (IMongoMapper)Activator.CreateInstance(mapperType);
mapper.Configure();
}
You can also plug any parameters you need to create an instance of your objects:
IConfiguration _configuration;
public Startup(IConfiguration configuration)
{
_configuration = configuration;
}
//other code
foreach(var mapperType in mapperTypes)
{
var mapper = (IMongoMapper)Activator.CreateInstance(mapperType, _configuration);
mapper.Configure();
}
There is also this question that has lots of examples (some do not work in dotnet core anymore): Getting all types that implement an interface
In Startup.cs
public void ConfigureServices(IServiceCollection services)
{
services.AddScoped<IPersonals, Personals>();
}

Converting Hangfire into modular startup

I am converting my startup code into new ServiceStack Modular Startup approach and have hit a snag.
I have this code in old startup
public void Configure(IApplicationBuilder app)
{
app.UseHangfireDashboard(hangfirePath, new DashboardOptions
{
Authorization = new[] { new HangFireAuthorizationFilter() }
});
var appHost = new AppHost
{
AppSettings = settings
};
app.UseServiceStack(appHost);
var container = appHost.Resolve<Container>();
GlobalConfiguration.Configuration.UseActivator(new ContainerJobActivator(container));
app.UseHangfireServer();
}
It's important that app.UseHangfireDashboard is registered before app.UseServiceStack or the dashboard wont work.
I got it working all fine except for the part where it links the IoC container to hangfire:
GlobalConfiguration.Configuration.UseActivator(new ContainerJobActivator(container));
This is the working code without linking container:
[Priority(-1)]
public class ConfigureHangfirePostgreSql : IConfigureServices, IConfigureApp
{
IConfiguration Configuration { get; }
public ConfigureHangfirePostgreSql(IConfiguration configuration) => Configuration = configuration;
public void Configure(IServiceCollection services)
{
var conn = Configuration.GetValue<string>("database:connectionString");
services.AddHangfire((isp, config) =>
{
config.UsePostgreSqlStorage(conn, new PostgreSqlStorageOptions
{
InvisibilityTimeout = TimeSpan.FromDays(1)
});
config.UseConsole();
});
}
public void Configure(IApplicationBuilder app)
{
app.UseHangfireDashboard("/hangfire", new DashboardOptions
{
Authorization = new[] { new HangFireAuthorizationFilter() }
});
// commented out because I dont think it's possible to get container yet and also code doesn't work
//var container = app.Resolve<Container>();
//GlobalConfiguration.Configuration.UseActivator(new ContainerJobActivator(container));
app.UseHangfireServer();
}
}
I am setting the priority to -1 so it runs before servicestack is registered. Because of that I guess the container isn't yet created so I need to make another module to run after like this:
[Priority(2)]
public class ConfigureHangfirePostgreSqlPost : IConfigureApp
{
public void Configure(IApplicationBuilder app)
{
//how do I resolve container here?
}
}
The container isn't registered as a service (as I am asking the container for the service) so im not sure how I am able to access it.
What is the right way of getting hold of the container in a startup module?
It's hard to decipher what the actual question is, answering a clear one found on the last line:
What is the right way of getting hold of the container in a startup module?
This existing answer has a good summary of ASP.NET Core IOC, where you can access any singleton dependencies from app.ApplicationServices, e.g:
public void Configure(IApplicationBuilder app)
{
var serviceProvider = app.ApplicationServices;
var hostingEnv = serviceProvider.GetService<IHostingEnvironment>();
}

Adding additional services to IServiceCollection in Startup class for Integration test in asp.net core 2.0

I want to add a few more Services in IServiceCollection Object from my Integration test project to the Startup.cs.
I am adding my TestConfiguration in the ConfigureServices method of the WebHostBuilder of my integration test, but its called before my startup class so my test configuration gets overridden.
Test class TestSetup.cs
var builder = new WebHostBuilder()
.UseEnvironment("development")
.UseContentRoot(contentRoot)
.ConfigureServices(TestConfigureServices)
.UseStartup<Startup>();
private void TestConfigureServices(IServiceCollection serviceCollection)
{
serviceCollection.AddSingleton((c) =>
new TokenClient(TokenEndpoint,
"api.public.client",
"psdfsrfsdf",
innerHttpMessageHandler: _handler));
}
API Project Startup.cs
public void ConfigureServices(IServiceCollection services)
{
services.AddSingleton((b) =>
new TokenClient(m.AccessTokenUrl,
SecurityConfig.PublicApiClientId,
SecurityConfig.PublicApiClientPassword));
}
I think you need to create different Startup classes for Production and Tests.
Then the necessary Startup will "choosen" depending on the current build.
Example:
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
services.AddTransient<IYourService, YourService>();
}
}
public class StartupTesting
{
public void ConfigureServices(IServiceCollection services)
{
services.AddTransient<IYourService, YourMockedService>();
}
}
var assemblyName = typeof(Startup).GetTypeInfo().Assembly.FullName;
//var assemblyName = typeof(StartupTesting).GetTypeInfo().Assembly.FullName;
var host = new WebHostBuilder()
.UseKestrel()
.UseContentRoot(Directory.GetCurrentDirectory())
.UseStartup(assemblyName)
.Build();
host.Run();
Updated:
There are second approach - using environment specific methods in one Startup class. But my humble opinion is that this causes too much coupling between the tests and the staging code.
Example you can see here
Since ASP Core 2.1 there is a ConfigureTestServices method available on WebHostBuilder that runs after registrations from Startup.cs. So your code would look like this:
var builder = new WebHostBuilder()
.UseEnvironment("development")
.UseContentRoot(contentRoot)
.ConfigureTestServices(TestConfigureServices)
.UseStartup<Startup>();

How rebind service in TestHost

I am preparing environment tests for my application. And I have problem how can i rebind earlier registred services in my startup class?.
I am using TestHost in my tests and this is how looks my base class
public abstract class IntegrationTestBase : IDisposable
{
private readonly TestServer _server;
public IntegrationTestBase()
{
var webHostBuilder = new WebHostBuilder()
.UseStartup<Startup>();
_server = new TestServer(webHostBuilder);
}
public HttpClient CreateClient()
{
return _server.CreateClient();
}
public virtual void Dispose()
{
_server.Dispose();
}
}
The way I've been handling overriding the registration for integration tests is:
Make the ConfigureServices method on your Startup class virtual.
Create a derived class of Startup: e.g: IntegrationTestStartup
Override ConfigureService like:
override ConfigureServices(IServiceCollection s)
{
base.ConfigureServices(s);
s.AddSingleton<IService, IMockService>();
}
Since the last registration you make of a service is the one provided by the DI container when a constructor injection happens, this works well.
In case you depend on IEnumerable<IService>, all components registered for that service will be resolved.
If you can't make your ConfigureServices from the Startup class virtual I assume using new would do the job since the WebHostBuilder creates an instance of the type passed to it.
Now you can build your WebHostBuilder with:
var webHostBuilder = new WebHostBuilder()
.UseStartup<IntegrationTestStartup>();
Does this help or could you be more specific what kind of 'rebind' do you need?

Injecting Dependency programmatically asp.net core

I'm just starting with Asp.net core Dependency Injection, and my concept could be inaccurate. This docs.asp.net post explains how to inject context to a controller. I have little confusion regarding injection, in testing perspective. Assume we have following scenario:
public interface ITasksRepository
{
public void Create();
}
//This is fake implementation, using fake DbContext for testing purpose
public class TasksRepositoryFake : ITasksRepository
{
public void Create()
{
FakeDbContext.Add(sometask);
//logic;
}
}
//This is actual implementation, using actual DbContext
public class TasksRepository : ITasksRepository
{
public void Create()
{
DbContext.Add(someTask);
//logic;
}
}
Now in order to inject context in controller, we design it as:
public class TasksController : Controller
{
public ITasksRepository TaskItems { get; set; }
public TodoController(ITaskRepository taskItems)
{
TaskItems = taskItems;
}
//other logic
}
What asp.net core provides as builtin feature is, we can register the dependency injection in startup class as follows:
public void ConfigureServices(IServiceCollection services)
{
// Add framework services.
services.AddMvc();
services.AddSingleton<ITasksRepository, TasksRepositoryFake>();
}
According to this logic, my TaskRepositoryFake will be injected to the controller. So far, everything is clear. My questions/confusions regarding this are as follows:
Questions:
How can I use this builtin DI feature to inject the context using some logic? May be programatically, or configuration based, or environment based? (for example, always inject fake context, when using 'test' environment? etc.)
Is it even possible? If we always have to change this manually in StartUp class, then how does this builtin DI feature serve us? Because we could have simply done that in controller, without this feature.
First to answer your question: Yes, you can inject the dependency programmatically. By using factories, as is always the case with injecting dependencies based on run-time values. The AddSingleton has an overload which takes an implementationfactory and so a basic example for your use case looks like:
public class Startup
{
public bool IsTesting { get; }
public Startup(IHostingEnvironment env)
{
IsTesting = env.EnvironmentName == "Testing";
}
// This method gets called by the runtime. Use this method to add services to the container.
// For more information on how to configure your application, visit http://go.microsoft.com/fwlink/?LinkID=398940
public void ConfigureServices(IServiceCollection services)
{
services.AddSingleton<ISomeRepository>(sp => IsTesting ? (ISomeRepository)new SomeRepository() : (ISomeRepository) new FakesomeRepository());
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, ISomeRepository someRepository)
{
app.UseIISPlatformHandler();
app.Run(async (context) =>
{
await context.Response.WriteAsync($"Hello World from {nameof(someRepository)}!");
});
}
// Entry point for the application.
public static void Main(string[] args) => WebApplication.Run<Startup>(args);
}
The concerning line of code for your TasksRepository would look like:
services.AddSingleton<ITaskRepository>(sp => isTesting?(ITasksRepository)new TasksRepositoryFake(): (ITasksRespository)new TasksRepository() );
Even better would be to put it in a factory (again with my example):
services.AddSingleton<ISomeRepository>(sp => SomeRepositoryFactory.CreatSomeRepository(IsTesting));
I hope you see how this helps you setting it up config based, environment based, or however you want.
I you are interested I wrote more about DI based on run-time values via abstract factories here.
Having said that, with unit tests I would simply inject my fakes in the classes that are under test. Unit tests are there to still prove to yourself and your co-workers that the code still does as intended.
And with integration tests I would make a special StartUp class with all my fakes and give it to the test host as ASP.NET Core allows you to do. You can read more about the test host here: https://docs.asp.net/en/latest/testing/integration-testing.html
Hope this helps.
Update Added cast to interface because the ternary conditional has no way of telling. Plus added some basic samples.
You can inject your dependencies configuration based, or environment based, or both.
Option 1 : Environment Based
public IHostingEnvironment env{ get; set; }
public Startup(IHostingEnvironment env)
{
this.env = env;
}
public void ConfigureServices(IServiceCollection services)
{
if (env.IsDevelopment())
{
// register other fake dependencies
services.AddSingleton<ITasksRepository, TasksRepositoryFake>();
}
else
{
// register other real dependencies
services.AddSingleton<ITasksRepository, TasksRepository>();
}
}
Option 2 : Configuration Based
public IConfigurationRoot Configuration { get; set; }
public Startup()
{
var builder = new ConfigurationBuilder()
.SetBasePath(env.ContentRootPath)
.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true)
.AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true)
.AddEnvironmentVariables();
Configuration = builder.Build();
}
public void ConfigureServices(IServiceCollection services)
{
var isFakeMode= Configuration["ServiceRegistrationMode"] == "Fake";
if (isFakeMode)
{
// register other fake dependencies
services.AddSingleton<ITasksRepository, TasksRepositoryFake>();
}
else
{
// register other real dependencies
services.AddSingleton<ITasksRepository, TasksRepository>();
}
}
Option 3 : Environment Based + Configuration Based
public IConfigurationRoot Configuration { get; set; }
public IHostingEnvironment env{ get; set; }
public Startup(IHostingEnvironment env)
{
this.env = env;
var builder = new ConfigurationBuilder()
.SetBasePath(env.ContentRootPath)
.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true)
.AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true)
.AddEnvironmentVariables();
Configuration = builder.Build();
}
public void ConfigureServices(IServiceCollection services)
{
var isFakeMode = Configuration["ServiceRegistrationMode"] == "Fake";
if (env.IsDevelopment() && isFakeMode)
{
// register other fake dependencies
services.AddSingleton<ITasksRepository, TasksRepositoryFake>();
}
else
{
// register other real dependencies
services.AddSingleton<ITasksRepository, TasksRepository>();
}
}

Categories