在第 3 方库中伪造扩展方法
Faking an Extension Method in a 3rd Party Library
我写了一个无法测试的方法吗?由于我正在使用的库有一个作为扩展方法实现的重要方法,所以我似乎无法伪造它。因此,无法测试我的方法。
首先,我将列出我要测试的方法的截断版本。
然后,我将列出我使用 FakeItEasy 伪造它的尝试。
该方法使用缓存,它是对缓存库中的静态方法的调用 LazyCache
我正在努力伪造:
public async Task<BassRuleEditModel> GetBassRuleEditModel(
int facilityId,
int criteriaId,
int bassRuleId,
BassRuleEditDto bassRuleEditDto)
{
var url = _bassRuleService.GetServiceConnectionForFacility(facilityId).Url;
var dto = bassRuleEditDto ?? _bassRuleService.GetBassRuleEditDto(bassRuleId);
var bassRuleEditModel = new BassRuleEditModel
{
...
LocationList = await GetLocations(url),
...
};
...
return bassRuleEditModel;
}
private async Task<IEnumerable<SelectListItem>> GetLocations(string url)
{
var cacheKey = string.Concat(CacheKeys.Location, url);
var selectList = await _appCache.GetOrAddAsync(cacheKey, async () =>
{
return new SelectList(await _tasksAndPrioritiesService.ReturnLocationsAsync(url), NameProperty, NameProperty);
}
, CacheKeys.DefaultCacheLifetime);
return selectList;
}
是GetOrAddAsync
方法,是一种扩展方法。
我只想从缓存中将假 return 设为空 SelectList
。
请注意,AppCache
和所有依赖项都是使用构造函数注入注入的。
我编写的单元测试,其中我试图伪造 AppCache
是:
[Fact]
public async Task Un_Named_Test_Does_Stuff()
{
var url = "http://somesite.com";
var referrer = new Uri(url);
var facilityId = GetRandom.Id();
var serviceConnectionDto = new ServiceConnectionDto
{
Url = "http://google.com" // this url does not matter
};
var cacheKey = string.Concat(CacheKeys.Location, serviceConnectionDto.Url);
A.CallTo(() => _bassRuleService.GetServiceConnectionForFacility(facilityId)).Returns(serviceConnectionDto);
A.CallTo(() => _urlHelper.Content("~/ServiceSpec/ListView")).Returns(url);
A.CallTo(() => _appViewService.GetReferrer(url)).Returns(referrer);
A.CallTo(() => _appCache.GetOrAddAsync(cacheKey, A<Func<Task<SelectList>>>.Ignored))
.Returns(Task.FromResult(new SelectList(Enumerable.Empty<SelectListItem>().ToList())));
var editModel = await
_bassRuleService.GetBassRuleEditModel(GetRandom.Int32(),
GetRandom.Int32(),
GetRandom.Int32(),
null
);
var path = editModel.Referrer.AbsolutePath;
editModel.Referrer.AbsolutePath.ShouldBe(referrer.AbsolutePath);
}
我在测试的构造函数中创建了假货(使用 xUnit):
public BassRuleQueryServiceTests()
{
_currentUser = A.Fake<ICurrentUser>();
_bassRuleService = A.Fake<IBassRuleService>();
_tasksAndPrioritiesService = A.Fake<ITasksAndPrioritiesService>();
_appViewService = A.Fake<IAppViewService>();
_urlHelper = A.Fake<IUrlHelper>();
_applicationDateTime = A.Fake<IApplicationDateTime>();
_appCache = new MockCacheService();
}
来自 运行 测试的错误是:
Message:
FakeItEasy.Configuration.FakeConfigurationException :
The current proxy generator can not intercept the method LazyCache.AppCacheExtenions.GetOrAddAsync1[Microsoft.AspNetCore.Mvc.Rendering.SelectList](LazyCache.IAppCache cache, System.String key,
System.Func
1[System.Threading.Tasks.Task`1[Microsoft.AspNetCore.Mvc.Rendering.SelectList]] addItemFactory) for the following reason:
- Extension methods can not be intercepted since they're static.>
我知道伪静态方法不可用。我正在寻找解决方案。
我是否需要向库作者施加压力使其不使用扩展方法? (滑稽的问题)
干杯
正如您正确指出的那样,扩展是静态方法,无法伪造静态方法。
扩展方法通常只是包装器,以简化对它们扩展的类型的操作;这里似乎就是这种情况。您正在调用的 GetOrAddAsync
扩展方法最终会调用 IAppCache.GetOrAddAsync
method。所以你应该伪造 that 方法。
A.CallTo(() => _appCache.GetOrAddAsync(cacheKey, A<Func<ICacheEntry, Task<SelectList>>>.Ignored))
.Returns(new SelectList(Enumerable.Empty<SelectListItem>().ToList()));
不是很方便,因为这意味着你需要知道扩展方法是做什么的,但是没有办法绕过它(除了围绕库创建一个抽象层,但是LazyCache已经是一个抽象了Microsoft.Extensions.Caching.Memory...)
(顺便说一句,您不需要 Task.FromResult
;Returns
方法有一个重载,当您配置方法 return 时接受 T
Task<T>
)
此外,如果您要 return 一个空序列,则根本不需要配置该方法。 FakeItEasy 的默认行为是 return 一个空的虚拟 IEnumerable<SelectListItem>
。
作为@Thomas Levesque 的出色回答的替代方案,另外两个替代方案是:
- 根本不要模拟缓存 - 使用真正的
CachingService
,因为它在内存中运行,因此包含在测试中是完全合理的。
- 为此使用 LazyCache 附带的模拟实例
MockCachingService
缓存。
有关示例,请参阅 https://github.com/alastairtree/LazyCache/wiki/Unit-testing-code-using-LazyCache。
我写了一个无法测试的方法吗?由于我正在使用的库有一个作为扩展方法实现的重要方法,所以我似乎无法伪造它。因此,无法测试我的方法。
首先,我将列出我要测试的方法的截断版本。 然后,我将列出我使用 FakeItEasy 伪造它的尝试。
该方法使用缓存,它是对缓存库中的静态方法的调用 LazyCache
我正在努力伪造:
public async Task<BassRuleEditModel> GetBassRuleEditModel(
int facilityId,
int criteriaId,
int bassRuleId,
BassRuleEditDto bassRuleEditDto)
{
var url = _bassRuleService.GetServiceConnectionForFacility(facilityId).Url;
var dto = bassRuleEditDto ?? _bassRuleService.GetBassRuleEditDto(bassRuleId);
var bassRuleEditModel = new BassRuleEditModel
{
...
LocationList = await GetLocations(url),
...
};
...
return bassRuleEditModel;
}
private async Task<IEnumerable<SelectListItem>> GetLocations(string url)
{
var cacheKey = string.Concat(CacheKeys.Location, url);
var selectList = await _appCache.GetOrAddAsync(cacheKey, async () =>
{
return new SelectList(await _tasksAndPrioritiesService.ReturnLocationsAsync(url), NameProperty, NameProperty);
}
, CacheKeys.DefaultCacheLifetime);
return selectList;
}
是GetOrAddAsync
方法,是一种扩展方法。
我只想从缓存中将假 return 设为空 SelectList
。
请注意,AppCache
和所有依赖项都是使用构造函数注入注入的。
我编写的单元测试,其中我试图伪造 AppCache
是:
[Fact]
public async Task Un_Named_Test_Does_Stuff()
{
var url = "http://somesite.com";
var referrer = new Uri(url);
var facilityId = GetRandom.Id();
var serviceConnectionDto = new ServiceConnectionDto
{
Url = "http://google.com" // this url does not matter
};
var cacheKey = string.Concat(CacheKeys.Location, serviceConnectionDto.Url);
A.CallTo(() => _bassRuleService.GetServiceConnectionForFacility(facilityId)).Returns(serviceConnectionDto);
A.CallTo(() => _urlHelper.Content("~/ServiceSpec/ListView")).Returns(url);
A.CallTo(() => _appViewService.GetReferrer(url)).Returns(referrer);
A.CallTo(() => _appCache.GetOrAddAsync(cacheKey, A<Func<Task<SelectList>>>.Ignored))
.Returns(Task.FromResult(new SelectList(Enumerable.Empty<SelectListItem>().ToList())));
var editModel = await
_bassRuleService.GetBassRuleEditModel(GetRandom.Int32(),
GetRandom.Int32(),
GetRandom.Int32(),
null
);
var path = editModel.Referrer.AbsolutePath;
editModel.Referrer.AbsolutePath.ShouldBe(referrer.AbsolutePath);
}
我在测试的构造函数中创建了假货(使用 xUnit):
public BassRuleQueryServiceTests()
{
_currentUser = A.Fake<ICurrentUser>();
_bassRuleService = A.Fake<IBassRuleService>();
_tasksAndPrioritiesService = A.Fake<ITasksAndPrioritiesService>();
_appViewService = A.Fake<IAppViewService>();
_urlHelper = A.Fake<IUrlHelper>();
_applicationDateTime = A.Fake<IApplicationDateTime>();
_appCache = new MockCacheService();
}
来自 运行 测试的错误是:
Message: FakeItEasy.Configuration.FakeConfigurationException : The current proxy generator can not intercept the method LazyCache.AppCacheExtenions.GetOrAddAsync
1[Microsoft.AspNetCore.Mvc.Rendering.SelectList](LazyCache.IAppCache cache, System.String key, System.Func
1[System.Threading.Tasks.Task`1[Microsoft.AspNetCore.Mvc.Rendering.SelectList]] addItemFactory) for the following reason: - Extension methods can not be intercepted since they're static.>
我知道伪静态方法不可用。我正在寻找解决方案。
我是否需要向库作者施加压力使其不使用扩展方法? (滑稽的问题)
干杯
正如您正确指出的那样,扩展是静态方法,无法伪造静态方法。
扩展方法通常只是包装器,以简化对它们扩展的类型的操作;这里似乎就是这种情况。您正在调用的 GetOrAddAsync
扩展方法最终会调用 IAppCache.GetOrAddAsync
method。所以你应该伪造 that 方法。
A.CallTo(() => _appCache.GetOrAddAsync(cacheKey, A<Func<ICacheEntry, Task<SelectList>>>.Ignored))
.Returns(new SelectList(Enumerable.Empty<SelectListItem>().ToList()));
不是很方便,因为这意味着你需要知道扩展方法是做什么的,但是没有办法绕过它(除了围绕库创建一个抽象层,但是LazyCache已经是一个抽象了Microsoft.Extensions.Caching.Memory...)
(顺便说一句,您不需要 Task.FromResult
;Returns
方法有一个重载,当您配置方法 return 时接受 T
Task<T>
)
此外,如果您要 return 一个空序列,则根本不需要配置该方法。 FakeItEasy 的默认行为是 return 一个空的虚拟 IEnumerable<SelectListItem>
。
作为@Thomas Levesque 的出色回答的替代方案,另外两个替代方案是:
- 根本不要模拟缓存 - 使用真正的
CachingService
,因为它在内存中运行,因此包含在测试中是完全合理的。 - 为此使用 LazyCache 附带的模拟实例
MockCachingService
缓存。
有关示例,请参阅 https://github.com/alastairtree/LazyCache/wiki/Unit-testing-code-using-LazyCache。