Closed drauch closed 5 years ago
Hi @drauch , there's no built-in utility I'm aware of to do this automatically.
When you tried to instantiate an HtmlHelper can you show us where you got stuck?
In the meantime I actually got to instantiate an HtmlHelper (the hardest part being instantiating the required RazorViewEngine object).
However, when I run my unit test it looks like htmlHelper.Editor(...)
internally tries to obtain another HtmlHelper from the RequestServices collection - StackTrace:
System.ArgumentNullException : Value cannot be null.
Parameter name: provider
at Microsoft.Extensions.DependencyInjection.ServiceProviderServiceExtensions.GetRequiredService[T](IServiceProvider provider)
at Microsoft.AspNetCore.Mvc.ViewFeatures.Internal.TemplateRenderer.MakeHtmlHelper(ViewContext viewContext, ViewDataDictionary viewData)
[...]
Is there a way to mitigate this problem? Do I have to register my HtmlHelper with the container as well? Why is htmlHelper.Editor() internally instantiating yet another HtmlHelper object?
My code (for any interested reader):
protected IHtmlHelper CreateHtmlHelper (IModelMetadataProvider modelMetadataProvider)
{
var mvcViewOptionsValue = new MvcViewOptions();
var mvcViewOptions = A.Fake<IOptions<MvcViewOptions>>();
A.CallTo(() => mvcViewOptions.Value).Returns(mvcViewOptionsValue);
mvcViewOptions.Value.ViewEngines.Add(CreateViewEngine(modelMetadataProvider));
var viewEngine = new CompositeViewEngine(mvcViewOptions);
var bufferScope = new MemoryPoolViewBufferScope(ArrayPool<ViewBufferValue>.Shared, ArrayPool<char>.Shared);
return new HtmlHelper(
CreateHtmlGenerator(modelMetadataProvider),
viewEngine,
modelMetadataProvider,
bufferScope,
HtmlEncoder.Default,
UrlEncoder.Default);
}
private IViewEngine CreateViewEngine (IModelMetadataProvider modelMetadataProvider)
{
var fileProvider = new PhysicalFileProvider("C:\\"); // some path, we do not want to locate non-default views anyways
var razorViewEngineOptionsValue = new RazorViewEngineOptions();
razorViewEngineOptionsValue.FileProviders.Add(fileProvider);
razorViewEngineOptionsValue.ViewLocationFormats.Add("Views/{1}/{0}");
razorViewEngineOptionsValue.AreaViewLocationFormats.Add("Areas/{2}/{1}/{0}");
var razorViewEngineOptions = A.Fake<IOptions<RazorViewEngineOptions>>();
A.CallTo(() => razorViewEngineOptions.Value).Returns(razorViewEngineOptionsValue);
var applicationPartManager = new ApplicationPartManager();
var defaultRazorViewEngineFileProviderAccessor = new DefaultRazorViewEngineFileProviderAccessor(razorViewEngineOptions);
var loggerFactory = new LoggerFactory();
var razorCompilationService = new RazorCompilationService(
new DefaultRoslynCompilationService(
new CSharpCompiler(new RazorReferenceManager(applicationPartManager, razorViewEngineOptions), razorViewEngineOptions),
defaultRazorViewEngineFileProviderAccessor,
razorViewEngineOptions,
loggerFactory),
new MvcRazorHost(new DefaultChunkTreeCache(fileProvider), new TagHelperDescriptorResolver(true)),
defaultRazorViewEngineFileProviderAccessor,
loggerFactory);
var pageFactory = new DefaultRazorPageFactoryProvider(
razorCompilationService,
new DefaultCompilerCacheProvider(applicationPartManager, defaultRazorViewEngineFileProviderAccessor));
var pageActivator = new RazorPageActivator(
modelMetadataProvider,
new UrlHelperFactory(),
new JsonHelper(new JsonOutputFormatter(new JsonSerializerSettings(), ArrayPool<char>.Shared), ArrayPool<char>.Shared),
new DiagnosticListener("Test"),
HtmlEncoder.Default,
new ModelExpressionProvider(modelMetadataProvider, new ExpressionTextCache()));
return new RazorViewEngine(pageFactory, pageActivator, HtmlEncoder.Default, razorViewEngineOptions, loggerFactory);
}
Note that CreateHtmlGenerator() is missing, take a look at https://github.com/aspnet/Mvc/blob/rel/2.0.0-preview2/test/Microsoft.AspNetCore.Mvc.TagHelpers.Test/TestableHtmlGenerator.cs for how to instantiate an HtmlGenerator.
Why is
htmlHelper.Editor()
internally instantiating yet anotherHtmlHelper
object?
HtmlHelper
instances are context-specific and cannot be shared as ViewContext
values change.
Do I have to register my
HtmlHelper
with the container as well?
Yes.
@dougbu can you take a crack at this and maybe we can come up with at least a sample of how to do this? This is somewhat tangentially related to "making testing better" in MVC. We expect plenty of people to write tag helpers and HTML helpers so hopefully there's something we can do to make this experience a bit smoother.
@By setting the TagHelper's ViewContext to an appropriate ViewContext instance (see code below), contextualizing and using the HtmlHelper in my tag helper is possible in an integrative way.
protected ViewContext CreateViewContext (IModelMetadataProvider modelMetadataProvider)
{
var builder = new ContainerBuilder();
builder.Register(ctx => CreateHtmlHelper(modelMetadataProvider)).As<IHtmlHelper>();
var container = builder.Build();
var serviceProvidersFeature = new ServiceProvidersFeature();
serviceProvidersFeature.RequestServices = new AutofacServiceProvider(container);
var featureCollection = new FeatureCollection();
featureCollection.Set<IServiceProvidersFeature>(serviceProvidersFeature);
var defaultHttpContext = new DefaultHttpContext(featureCollection);
var actionContext = new ActionContext(defaultHttpContext, new RouteData(), new ActionDescriptor());
var view = new NullView();
var viewData = new ViewDataDictionary(modelMetadataProvider, new ModelStateDictionary());
var tempData = A.Dummy<ITempDataDictionary>();
var writer = TextWriter.Null;
var htmlHelperOptions = new HtmlHelperOptions();
return new ViewContext(actionContext, view, viewData, tempData, writer, htmlHelperOptions);
}
I'm using the Autofac container as IServiceProvider.
Although this works, it's been a LOT of very intense work for something which should be very easy I in order to maximize testability of tag helpers and html helpers.
Oh, just realized it is not really working 100% yet. HtmlHelper.Editor() does not yet use the correct templates ... my guess is I'm building up my model explorer in a wrong way. Does this look correct to you?
protected ModelExplorer CreateModelExplorerForType (IModelMetadataProvider modelMetadataProvider, Type modelType)
{
var dataAnnotationsLocalizationOptions = A.Fake<IOptions<MvcDataAnnotationsLocalizationOptions>>();
A.CallTo(() => dataAnnotationsLocalizationOptions.Value).Returns(new MvcDataAnnotationsLocalizationOptions());
var localizationOptions = A.Fake<IOptions<LocalizationOptions>>();
A.CallTo(() => localizationOptions.Value).Returns(new LocalizationOptions());
var bindingMetadataProvider = new DefaultBindingMetadataProvider();
var displayMetadataProvider = new DataAnnotationsMetadataProvider(
dataAnnotationsLocalizationOptions,
new ResourceManagerStringLocalizerFactory(new HostingEnvironment(), localizationOptions));
var validationMetadataProvider = new DefaultValidationMetadataProvider();
var detailsProvider = new DefaultCompositeMetadataDetailsProvider(
new IMetadataDetailsProvider[] { bindingMetadataProvider, displayMetadataProvider, validationMetadataProvider });
var details = new DefaultMetadataDetails(ModelMetadataIdentity.ForType(modelType), ModelAttributes.GetAttributesForType(modelType));
var defaultModelMetadata = new DefaultModelMetadata(modelMetadataProvider, detailsProvider, details);
return new ModelExplorer(modelMetadataProvider, defaultModelMetadata, null);
}
@DamianEdwards FYI
I'm stuck here as well.
I can't figure out how to create an instance of IHtmlHelper
We ultimately decided to use an IHtmlHelper
interface in our tag helpers and simply mock it for our tag helper unit tests. This is somewhat bad, because we can't really test some stuff, e.g., that the correct editor template is rendered, but it is better than no tests for our tag helpers.
We ultimately decided to use an
IHtmlHelper
interface in our tag helpers and simply mock it for our tag helper unit tests. This is somewhat bad, because we can't really test some stuff, e.g., that the correct editor template is rendered, but it is better than no tests for our tag helpers.
This sounds exactly what we would suggest. For the rest we would recommend using integration tests: https://docs.microsoft.com/en-us/aspnet/core/test/integration-tests?view=aspnetcore-3.0#test-app-prerequisites
We are writing our own tag helpers which make use of
HtmlHelper
internally (e.g., to render an Editor). This seems to be the recommended behavior, see also https://docs.microsoft.com/en-us/aspnet/core/mvc/views/tag-helpers/intro#tag-helpers-compared-to-html-helpersFor unit testing purposes we could "rather easily" instantiate an HtmlHelper in ASP.NET MVC 5 (e.g., see https://stackoverflow.com/questions/25206507/unit-testing-htmlhelper-extension-method-fails/25216750#25216750). This is no longer the case in ASP.NET Core. Is there a utility or some other easy way to instantiate an HtmlHelper for a unit test?