TryUpdateModelAsync is an internal protected method of abstract class PageModel, which makes it, to my view, unmockable. Is there any approach to unit test any of the action that involves this method?
I saw this solution on the internet: asp.net core mvc controller unit testing when using TryUpdateModel.
However, it only applies to ASP.NET Core MVC Web App since TryUpdateModelAsync is a public method, so we can add an adapator to wrap this method and make our adaptor call the actual method. In Razor Page, this TryUpdateModelAsync is inaccessible from outside.
The problem I was having right now is when I was unit testing the OnPostAsync method, TryUpdateModelAsync
always throw an exception. I could never pass through that line to check the code logic after it.
So here is the action I want to unit test:
public class CreateModel : PageModel
{
//elided
[BindProperty]
public Post Post { get; set; }
public async Task<IActionResult> OnPostAsync()
{
if (!ModelState.IsValid)
{
return Page();
}
if (await TryUpdateModelAsync<Post>(Post, "Post", p => p.Title, p => p.SubTitle, p => p.Author, p => p.Content, p => p.CategoryID)){
//do stuff
return RedirectToPage("./Index");
}
//do stuff
return Page();
}
Here's the unit test I wrote for it:
[Test]
public async Task InvokeOnPostAsyncWithInvalidModelState_ShouldReturnPageResultType(){
//Arrange
InitPostModelProperties();
var createModel = new CreateModel(){
PageContext = _pageContext,
TempData = _tempData,
Url = _urlHelper
};
//do arrangement
//Act
var result = await createModel.OnPostAsync();
//Assert
}
UPDATE:
Here's the full initialization process I did for this test:
public void InitPostModelProperties(){
_httpContext = new DefaultHttpContext(){
//RequestServices = services.BuildServiceProvider()
};
_modelStateDictionary = new ModelStateDictionary();
_modelMetaDataProvider = new EmptyModelMetadataProvider();
_actionContext = new ActionContext(_httpContext, new RouteData(), new PageActionDescriptor(), _modelStateDictionary);
_viewData = new ViewDataDictionary(_modelMetaDataProvider, _modelStateDictionary);
_tempData = new TempDataDictionary(_httpContext, Mock.Of<ITempDataProvider>());
_pageContext = new PageContext(_actionContext){
ViewData = _viewData
};
_urlHelper = new UrlHelper(_actionContext);
}
Then I got following exception message:
Error Message:
System.ArgumentNullException : Value cannot be null.
Parameter name: metadataProvider
Stack Trace:
at Microsoft.AspNetCore.Mvc.ModelBinding.Internal.ModelBindingHelper.TryUpdateModelAsync(Object model, Type modelType, String prefix, ActionContext actionContext, IModelMetadataProvider metadataProvider, IModelBinderFactory modelBinderFactory, IValueProvider valueProvider, IObjectModelValidator objectModelValidator, Func`2 propertyFilter)
at Microsoft.AspNetCore.Mvc.RazorPages.PageModel.TryUpdateModelAsync[TModel](TModel model, String name, Expression`1[] includeExpressions)
Then I changed my initialization to the following:
public void InitPostModelProperties(){
var services = new ServiceCollection();
services.AddTransient<IModelMetadataProvider, ModelMetadataProvider>();
_httpContext = new DefaultHttpContext(){
RequestServices = services.BuildServiceProvider()
};
_modelStateDictionary = new ModelStateDictionary();
_modelMetaDataProvider = new EmptyModelMetadataProvider();
_actionContext = new ActionContext(_httpContext, new RouteData(), new PageActionDescriptor(), _modelStateDictionary);
_viewData = new ViewDataDictionary(_modelMetaDataProvider, _modelStateDictionary);
_tempData = new TempDataDictionary(_httpContext, Mock.Of<ITempDataProvider>());
_pageContext = new PageContext(_actionContext){
ViewData = _viewData
};
_urlHelper = new UrlHelper(_actionContext);
}
Then I got new exception message:
Outcome: Failed
Error Message:
System.InvalidOperationException : No service for type 'Microsoft.AspNetCore.Mvc.ModelBinding.IModelMetadataProvider' has been registered.
Stack Trace:
at Microsoft.Extensions.DependencyInjection.ServiceProviderServiceExtensions.GetRequiredService(IServiceProvider provider, Type serviceType)
at Microsoft.Extensions.DependencyInjection.ServiceProviderServiceExtensions.GetRequiredService[T](IServiceProvider provider)
at Microsoft.AspNetCore.Mvc.RazorPages.PageModel.get_MetadataProvider()
at Microsoft.AspNetCore.Mvc.RazorPages.PageModel.TryUpdateModelAsync[TModel](TModel model, String name, Expression`1[] includeExpressions)
I followed the exception message and added these lines:
services.AddSingleton<IModelMetadataProvider, EmptyModelMetadataProvider>();
Got new exception message:
Outcome: Failed
Error Message:
System.InvalidOperationException : No service for type 'Microsoft.AspNetCore.Mvc.ModelBinding.IModelBinderFactory' has been registered.
Stack Trace:
at Microsoft.Extensions.DependencyInjection.ServiceProviderServiceExtensions.GetRequiredService(IServiceProvider provider, Type serviceType)
at Microsoft.Extensions.DependencyInjection.ServiceProviderServiceExtensions.GetRequiredService[T](IServiceProvider provider)
at Microsoft.AspNetCore.Mvc.RazorPages.PageModel.get_ModelBinderFactory()
at Microsoft.AspNetCore.Mvc.RazorPages.PageModel.TryUpdateModelAsync[TModel](TModel model, String name, Expression`1[] includeExpressions)
I kept adding new lines:
services.AddSingleton<IModelBinderFactory,ModelBinderFactory>();
Only to find the service can't be instansiated beause ModelBinderFactory class is an abstract class
I am stuck here.
UPDATE
The whole test code is shown below:
SRC:
public class BaseTests
{
private HugoBlogContext _context;
public HugoBlogContext Context => _context;
public List<Category> CatList;
public List<Post> PostList;
public List<Tag> TagList;
public List<PostTag> PostTagList;
public BaseTests(){}
public class PageModelBaseTests: BaseTests
{
protected ICategoryRepository _categoryRepository;
protected IPostRepository _postRepository;
protected IPostTagRepository _postTagRepository;
protected ITagRepository _tagRepository;
protected ISelectTagService _selectTagService;
protected IDropdownCategoryService _dropdownCategoryService;
protected HttpContext _httpContext;
protected ModelStateDictionary _modelStateDictionary;
protected ActionContext _actionContext;
protected ModelMetadataProvider _modelMetaDataProvider;
protected ViewDataDictionary _viewData;
protected TempDataDictionary _tempData;
protected PageContext _pageContext;
protected UrlHelper _urlHelper;
[SetUp]
public void Init(){
_categoryRepository = new CategoryRepository(Context);
_postRepository = new PostRepository(Context);
_postTagRepository = new PostTagRepository(Context);
_tagRepository = new TagRepository(Context);
_selectTagService = new SelectTagService(_tagRepository, _postRepository, _postTagRepository);
_dropdownCategoryService = new DropdownCategoryService(_categoryRepository);
}
public void InitPostModelProperties(){
var services = new ServiceCollection();
services.AddSingleton<IModelMetadataProvider, EmptyModelMetadataProvider>();
services.AddSingleton<IModelBinderFactory,ModelBinderFactory>();
_httpContext = new DefaultHttpContext(){
RequestServices = services.BuildServiceProvider()
};
_modelStateDictionary = new ModelStateDictionary();
_modelMetaDataProvider = new EmptyModelMetadataProvider();
_actionContext = new ActionContext(_httpContext, new RouteData(), new PageActionDescriptor(), _modelStateDictionary);
_viewData = new ViewDataDictionary(_modelMetaDataProvider, _modelStateDictionary);
_tempData = new TempDataDictionary(_httpContext, Mock.Of<ITempDataProvider>());
_pageContext = new PageContext(_actionContext){
ViewData = _viewData
};
_urlHelper = new UrlHelper(_actionContext);
}
[TestFixture]
public class CreateModelTests: PageModelBaseTests
{
[Test]
public async Task InvokeOnPostAsyncWithValidModelState_ShouldReturnRedirectPageResultTypeAndCategoryShouldBeUpdated(){
//Arrange
InitPostModelProperties();
var createModel = new CreateModel(_selectTagService, _dropdownCategoryService ,_postRepository){
PageContext = _pageContext,
TempData = _tempData,
Url = _urlHelper,
};
createModel.Post = new Post{
Title = "Create",
SubTitle = "Create",
Content = "Create",
Author = "Create",
CategoryID = CatList.Count + 1,
};
var selectedTags = new string[3]{"4","5","6"};
//Act
var result = await createModel.OnPostAsync(selectedTags);
//Assert
result.Should().BeOfType<RedirectToPageResult>();
Context.Posts.Where(c => c.CategoryID == (CatList.Count + 1)).Should().NotBeNull();
}
}
UPDATE After mocking, I got the following Exception Message:
Outcome: Failed
Error Message:
System.NullReferenceException : Object reference not set to an instance of an object.
Stack Trace:
at Microsoft.AspNetCore.Mvc.ModelBinding.Internal.ModelBindingHelper.TryUpdateModelAsync(Object model, Type modelType, String prefix, ActionContext actionContext, IModelMetadataProvider metadataProvider, IModelBinderFactory modelBinderFactory, IValueProvider valueProvider, IObjectModelValidator objectModelValidator, Func`2 propertyFilter)
at Microsoft.AspNetCore.Mvc.RazorPages.PageModel.TryUpdateModelAsync[TModel](TModel model, String name, Expression`1[] includeExpressions)
FYI: I also mocked IObjectValidator
by using services.AddSingleton<IObjectValidator>(Mock.Of<IObjectValidator>())
services.AddTransient<IModelBinderFactory, SOMETHING>();
? – SteinGateservices.AddSingleton<IModelBinderFactory>(Mock.Of<IModelBinderFactory>());
– NkosiObject reference not set to an instance of an object.
. But I can't tell which one isn't instansiated by jus looking at the exception meesage – SteinGate