ASP.NET Core MVC中IDataProtectionProvider相关单元测试异常求助
错误信息
Castle.DynamicProxy.InvalidProxyConstructorException : 无法创建POSModels.CryptoParamsProtector类的代理。未找到无参构造函数。
堆栈跟踪
ProxyGenerator.CreateClassProxyInstance(Type proxyType, List
1 proxyArguments, Type classToProxy, Object[] constructorArguments) ProxyGenerator.CreateClassProxy(Type classToProxy, Type[] additionalInterfacesToProxy, ProxyGenerationOptions options, Object[] constructorArguments, IInterceptor[] interceptors) CastleProxyFactory.CreateProxy(Type mockType, IInterceptor interceptor, Type[] interfaces, Object[] arguments) line 62 Mock1.InitializeInstance() line 311
Mock1.OnGetObject() line 325 Mock.get_Object() line 179 Mock1.get_Object() line 283
UnitTypesControllerTests.initController(Object obj) line 80
UnitTypesControllerTests.IndexTest() line 86
错误抛出点为initController方法的最后一行。以下是相关代码:
单元测试代码
namespace PWEB.Controllers.Tests { using Xunit; public class UnitTypesControllerTests { private Mock<IHttpContextAccessor> contextAccessor; private UnitTypesController unitTypesController; private Mock<NetCorePOSSystemConfig> config; private Mock<IHttpClientFactory> httpClientFactory; private Mock<Microsoft.Extensions.Options.IOptions<NetCorePOSSystemConfig>> configure; private Mock<Microsoft.AspNetCore.Http.HttpContext> mockHttpContext; private Mock<MockHttpMessageHandler> mock; private Mock<CryptoParamsProtector> cryptoParams; PaginationEntity<UnitTypeModel> paginationEntity = new PaginationEntity<UnitTypeModel>() { Items = new List<UnitTypeModel>(), MetaData = new PaginationMetaData() { Count = 1, TotalItemCount = 1, PageNumber = 1, FirstItemOnPage = 1, HasNextPage = false, HasPreviousPage = false, IsFirstPage = true, IsLastPage = false, LastItemOnPage = 1, PageCount = 1, PageSize = 20 } }; public UnitTypesControllerTests() { cryptoParams = new Mock<CryptoParamsProtector>(MockBehavior.Strict); mock = new Mock<MockHttpMessageHandler>(MockBehavior.Strict); } private void initController(object obj) { string value = "1"; byte[] val = new byte[] { 1 }; mock.Protected().Setup<Task<HttpResponseMessage>>("SendAsync", ItExpr.IsAny<HttpRequestMessage>(), ItExpr.IsAny<CancellationToken>()).ReturnsAsync(new HttpResponseMessage() { Content = new StringContent(Newtonsoft.Json.JsonConvert.SerializeObject(obj), System.Text.Encoding.UTF8, "application/json"), StatusCode = System.Net.HttpStatusCode.OK }).Verifiable(); var client = new HttpClient(mock.Object) { BaseAddress = new Uri("http://xxxxxxxxxxxxxx") }; httpClientFactory = new Moq.Mock<IHttpClientFactory>(); httpClientFactory.Setup(x => x.CreateClient(It.IsAny<string>())).Returns(client); config = new Mock<NetCorePOSSystemConfig>(); contextAccessor = new Mock<IHttpContextAccessor>(); mockHttpContext = new Mock<Microsoft.AspNetCore.Http.HttpContext>(); Mock<ISession> mocks = new Mock<ISession>(); mocks.Setup(c => c.Set("JWToken", It.IsAny<byte[]>())).Callback<string, byte[]>((k, v) => value = v.ToString()); mocks.Setup(v => v.TryGetValue("JWToken", out val)).Returns(true); mockHttpContext.Setup(v => v.Session).Returns(mocks.Object); contextAccessor.Setup(x => x.HttpContext).Returns(mockHttpContext.Object); configure = new Mock<Microsoft.Extensions.Options.IOptions<NetCorePOSSystemConfig>>(); unitTypesController = new UnitTypesController(cryptoParams.Object, httpClientFactory.Object, configure.Object, contextAccessor.Object); } [Fact()] public void IndexTest() { initController(paginationEntity); ViewResult result = unitTypesController.Index(1).Result as ViewResult; mock.Verify(); Assert.IsType<StaticPagedList<UnitTypeModel>>(result.ViewData.Model); } }
控制器代码
public class UnitTypesController : Controller { private readonly HttpClient _httpClient; private readonly IOptions<NetCorePOSSystemConfig> _options; private readonly IHttpContextAccessor contextAccessor; private readonly CryptoParamsProtector cryptoParams; public UnitTypesController(CryptoParamsProtector paramsProtector, IHttpClientFactory httpClientFactory, IOptions<NetCorePOSSystemConfig> config, IHttpContextAccessor httpContext) { //代码 .................. } }
加密类代码
public class CryptoParamsProtector { IDataProtector _protector; public CryptoParamsProtector(IDataProtectionProvider dataProtectionProvider) { _protector = dataProtectionProvider.CreateProtector(GetType().FullName); } // ........... }
解决方案
问题根源
Moq创建具体类的Mock对象时,默认要求目标类必须有无参构造函数。但你的CryptoParamsProtector类只定义了带IDataProtectionProvider参数的构造函数,没有无参构造函数,所以Moq无法生成代理对象,抛出该错误。
两种解决办法
办法一:抽象接口(推荐,符合依赖倒置原则)
- 创建
ICryptoParamsProtector接口,定义CryptoParamsProtector中的所有公共方法:
public interface ICryptoParamsProtector { // 这里添加CryptoParamsProtector中需要对外暴露的方法,例如: string Protect(string data); string Unprotect(string protectedData); }
- 修改
CryptoParamsProtector实现该接口:
public class CryptoParamsProtector : ICryptoParamsProtector { IDataProtector _protector; public CryptoParamsProtector(IDataProtectionProvider dataProtectionProvider) { _protector = dataProtectionProvider.CreateProtector(GetType().FullName); } // 实现接口中的方法 public string Protect(string data) { return _protector.Protect(data); } public string Unprotect(string protectedData) { return _protector.Unprotect(protectedData); } }
- 修改控制器的构造函数,依赖接口而非具体类:
public class UnitTypesController : Controller { private readonly HttpClient _httpClient; private readonly IOptions<NetCorePOSSystemConfig> _options; private readonly IHttpContextAccessor contextAccessor; private readonly ICryptoParamsProtector cryptoParams; // 参数改为ICryptoParamsProtector public UnitTypesController(ICryptoParamsProtector paramsProtector, IHttpClientFactory httpClientFactory, IOptions<NetCorePOSSystemConfig> config, IHttpContextAccessor httpContext) { // 原有代码 .................. } }
- 修改单元测试中的Mock对象为接口类型:
// 替换原来的Mock<CryptoParamsProtector> private Mock<ICryptoParamsProtector> cryptoParams; public UnitTypesControllerTests() { cryptoParams = new Mock<ICryptoParamsProtector>(MockBehavior.Strict); mock = new Mock<MockHttpMessageHandler>(MockBehavior.Strict); }
办法二:创建Mock时传入构造函数参数(临时方案,不推荐长期使用)
如果不想修改原有类结构,可以在创建CryptoParamsProtector的Mock时,传入其构造函数需要的参数(Mock的IDataProtectionProvider对象):
public UnitTypesControllerTests() { var mockDataProtectionProvider = new Mock<IDataProtectionProvider>(); // 如果需要用到_protector的方法,还需要继续Mock IDataProtector var mockDataProtector = new Mock<IDataProtector>(); mockDataProtectionProvider.Setup(p => p.CreateProtector(It.IsAny<string>())).Returns(mockDataProtector.Object); // 传入构造函数参数 cryptoParams = new Mock<CryptoParamsProtector>(mockDataProtectionProvider.Object, MockBehavior.Strict); mock = new Mock<MockHttpMessageHandler>(MockBehavior.Strict); }
这种方法虽然能解决报错,但会让测试代码更复杂,且不符合依赖倒置原则,建议优先使用办法一。
内容的提问来源于stack exchange,提问作者JagathNalin

