Bug? Lack of docs if it's already available.
I can seed data using WebHostBuilder using code like this:
https://github.com/dotnet-architecture/eShopOnWeb/blob/netcore2.1/src/Web/Program.cs#L16-L39
I'd like to be able to seed data before my functional tests. I'm using the new WebApplicationFactory<T> but am not seeing a simple way to hook into this.
I can configure the WebHostBuilder using code like this:
public class CatalogControllerIndex : IClassFixture<WebApplicationFactory<Startup>>
{
public CatalogControllerIndex(WebApplicationFactory<Startup> fixture)
{
var factory = fixture.Factories.FirstOrDefault() ?? fixture.WithWebHostBuilder(ConfigureWebHostBuilder);
Client = factory.CreateClient();
}
private static void ConfigureWebHostBuilder(IWebHostBuilder builder) =>
builder.UseEnvironment("Testing");
public HttpClient Client { get; }
...
However, I don't see how I can get to an actual instance of the built host, so that I can get to its dbContext, so that I can use it to add test data. Should I just try and do it using something like builder.Configure() ?
Microsoft.AspNetCore.Mvc or Microsoft.AspNetCore.App or Microsoft.AspNetCore.All:2.1
Tried using builder.Configure(), but of course that only works instead of, not in addition to, using UseStartup<Startup>(). So, that's out.
This looked promising, but Server is null here:
var factory = fixture.Factories.FirstOrDefault() ?? fixture.WithWebHostBuilder(ConfigureWebHostBuilder);
var host = factory.Server?.Host;
SeedData(host);
@danroth27 Have you used WebApplicationFactory with seeded data like this? Thanks in advance.
Ah ha! The Server property is populated after calling CreateClient() (obviously...). This works!
public class CatalogControllerIndex : IClassFixture<WebApplicationFactory<Startup>>
{
public CatalogControllerIndex(WebApplicationFactory<Startup> fixture)
{
var factory = fixture.Factories.FirstOrDefault() ?? fixture.WithWebHostBuilder(ConfigureWebHostBuilder);
Client = factory.CreateClient();
var host = factory.Server?.Host;
SeedData(host);
}
private void SeedData(IWebHost host)
{
if(host == null) { throw new ArgumentNullException("host"); }
using (var scope = host.Services.CreateScope())
{
var services = scope.ServiceProvider;
var loggerFactory = services.GetRequiredService<ILoggerFactory>();
try
{
var catalogContext = services.GetRequiredService<CatalogContext>();
CatalogContextSeed.SeedAsync(catalogContext, loggerFactory)
.Wait();
var userManager = services.GetRequiredService<UserManager<ApplicationUser>>();
AppIdentityDbContextSeed.SeedAsync(userManager).Wait();
}
catch (Exception ex)
{
var logger = loggerFactory.CreateLogger<CatalogControllerIndex>();
logger.LogError(ex, "An error occurred seeding the DB.");
}
}
}
private static void ConfigureWebHostBuilder(IWebHostBuilder builder)
{
builder.UseEnvironment("Testing");
}
public HttpClient Client { get; }
// tests
}
Thanks for contacting us, @ardalis. Looks like you've went through a good investigation here. Glad it worked out. After looking back now, it took you just an hour to figure it out. Which part specifically was the hard part? What/ how you'd like us to improve to make it even simpler?
/cc @danroth27, @javiercn
WebApplicationFactory doesn't actually have a CreateWebApplication method or a Create method that returns a WebApplication.
Server property has no way of being created that makes any sense. You need to create a Client and then realize that this in fact populates the Server. I made a PR to fix this. https://github.com/aspnet/Mvc/pull/7838
These were the two biggest issues. You don't want to have properties that magically do or don't have values based on other method calls that aren't related to them. You also don't want to use the Factory suffix name convention when the start of the type name isn't actually what the Factory is instantiating and returning from its Create method(s). If we can correct both of these design issues, it will make the type much easier and more intuitive. In the meantime, more docs including XML docs will help.
Realizing we're way late to get anything into 2.1 I think at a bare minimum you could raise an exception if someone tries to access the Server property, and in the exception explain that they need to call CreateClient in order to create the Server. But my PR is probably the better solution.
Thanks!
Also I thought "just an hour" comment was great. :) This should have been 30 seconds... 馃
Thanks for your feedback.
There's a point on the testing doc specifically on how to seed the database:
https://docs.microsoft.com/en-us/aspnet/core/test/integration-tests?view=aspnetcore-2.1#customize-webapplicationfactory
This package focuses on allowing developers to boot up your MVC app, there's really nothing we can do in this package to simplify how to seed data in EF, that said, it could be done in the same way that its done in a regular app. There are a couple of things that come to my mind though:
Can you tell us what you were doing to require accessing the Server property? We expect the common use case is to just create a client and start issuing requests.
Thanks for the docs link - that wasn't there a couple of days ago when I first was looking into this but it looks like good stuff from @GuardRex / Luke. I'll have a look at following that approach and will follow up.
You can see from this issue + comments how I was basically just trying to find where to hook in seeding of test data for my functional test. My previous (pre 2.1) tests did this in a base test class by getting access to the host and creating a scope and then getting a dbContext and seeding data (as you see in my later comments). In trying to do basically the same thing I was trying to get access to the host and its services collection. Server seemed promising, but was null, but then it wasn't if I attempted to access it after calling CreateClient. That's not intuitive.
Between the docs update and my current understanding of the WebApplicationFactory I'm all set, but I do still think it would be worth revisiting the naming and design of the type for the reasons I cited previously. Cheers.
Thanks,
I'm closing this for now as there's no action we can take here until our next major version.
Got into same issue with Server == null
We expect the common use case is to just create a client and start issuing requests.
This is a temporal dependency which is a very bad thing from design POV.
http://blog.ploeh.dk/2011/05/24/DesignSmellTemporalCoupling/
https://www.yegor256.com/2015/12/08/temporal-coupling-between-method-calls.html
I tried to seed data (specifically users) for integration testing with an inmemory database. I encounter the same error than the one explained here, server is null. I try to build a UserManager to call CreateAsync. I could not get the example above working. Any help is welcome.
public class CustomWebApplicationFactory<TStartup>
: WebApplicationFactory<TStartup> where TStartup : class
{
protected override void ConfigureWebHost(IWebHostBuilder builder)
{
builder.ConfigureServices(services =>
{
var serviceProvider = new ServiceCollection()
.AddEntityFrameworkInMemoryDatabase()
.BuildServiceProvider();
services.AddDbContext<DocIntelContext>(options =>
{
options.UseInMemoryDatabase("InMemoryDbForTesting");
options.UseInternalServiceProvider(serviceProvider);
});
});
}
}
public class BasicTests : IClassFixture<CustomWebApplicationFactory<Startup>>
{
protected readonly CustomWebApplicationFactory<Startup> _factory;
public BasicTests(CustomWebApplicationFactory<Startup> factory)
{}
[Fact]
public async void MyTest()
{
// Arrange
var client = _factory.CreateClient();
Assert.NotNull(_factory.Server);
}
}
I also tried the approach described here : https://docs.microsoft.com/en-us/aspnet/core/test/integration-tests?view=aspnetcore-2.1#customize-webapplicationfactory but I cannot get an instance of UserManager.
Most helpful comment
Ah ha! The
Serverproperty is populated after callingCreateClient()(obviously...). This works!