如何对包含 TryUpdateModelAsync 方法的 ASP.NET CORE Razor Page Web 应用程序进行单元测试?

3

TryUpdateModelAsync是抽象类PageModel的一个内部保护方法,我认为它不可模拟。是否有任何方法可以对涉及此方法的任何操作进行单元测试?

我在互联网上看到了这个解决方案: asp.net core mvc controller unit testing when using TryUpdateModel.

然而,它仅适用于ASP.NET Core MVC Web App,因为TryUpdateModelAsync是公共方法,所以我们可以添加适配器来包装此方法并使我们的适配器调用实际方法。在Razor Page中,此TryUpdateModelAsync从外部无法访问。

我遇到的问题是当我单元测试OnPostAsync方法时,TryUpdateModelAsync总是抛出异常。我永远不能通过那行代码检查后面的代码逻辑。

因此,这是我想要进行单元测试的操作:

 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();
        }

这是我为此编写的单元测试:
  [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
        }

更新:

以下是我为这个测试执行的完整初始化过程:

  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);
        }

接下来我收到了以下异常消息:

  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)

然后我将我的初始化更改为以下内容:

  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);
        }

然后我收到了一个新的异常消息:

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)

我遵循异常信息并添加了这些代码:

services.AddSingleton<IModelMetadataProvider, EmptyModelMetadataProvider>();

收到新的异常信息:

  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)

我不断添加新的行:

 services.AddSingleton<IModelBinderFactory,ModelBinderFactory>();

发现服务无法实例化,因为ModelBinderFactory类是一个抽象类。

我被卡在这里了。

更新

完整的测试代码如下:

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();
        }
    }

更新 模拟后,我得到了以下的异常信息:

  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)

提示:我还使用了services.AddSingleton<IObjectValidator>(Mock.Of<IObjectValidator>())模拟了IObjectValidator


1
你可以模拟抽象类并将其添加到服务集合中。 - Nkosi
我同意Nkosi的观点。模拟抽象类并将其添加到服务集合中。 - Simone Spagna
你的意思是使用 services.AddTransient<IModelBinderFactory, SOMETHING>(); 吗? - SteinGate
通过将模拟对象传递到服务集合中。services.AddSingleton<IModelBinderFactory>(Mock.Of<IModelBinderFactory>()); - Nkosi
有一个新的异常被抛出。它说“对象引用未设置为对象的实例”。但是仅凭异常消息,我无法确定哪个对象未被实例化。 - SteinGate
1个回答

0

我不确定测试TryValidateModelTryUpdateModelAsync是我们的责任;毕竟可以安全地假设框架方法是正确的,对吧。

另一个论点是你正在测试你自己的实现而不是别人的。

无论如何,如果您想忽略这些方法,您可以在测试方法中尝试这个(我使用Moq作为我的Mocking Framework),完全忽略这些方法并且实际上假设它们是正确的:

var objectModelValidatorMock = new Mock<IObjectModelValidator>();
objectModelValidatorMock
    .Setup(o => o.Validate(
        It.IsAny<ActionContext>(),
        It.IsAny<ValidationStateDictionary>(),
        It.IsAny<string>(),
        It.IsAny<object>()));

var sut = new NewController
{
    ObjectValidator = objectModelValidatorMock.Object
};

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接