unit-testingfakeiteasylazycache

Faking an Extension Method in a 3rd Party Library


Have I written an untestable method? As the library I am using has an important method which is implemented as an extension method, it seems like I am unable to fake it. And thus, unable to test my method.

First, I'll set out a truncated version of the method I want to test. Then, I'll set out the attempt I have made to fake it using FakeItEasy.

The method uses caching and it is the call to the static method in the caching library LazyCache which I am struggling to fake:

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

It is the GetOrAddAsync method which is an extension method.
I just want the fake to return from the cache an empty SelectList.

Note, the AppCache and all dependencies are injected using constructor injection.

The unit test which I have written, where I have tried to fake the AppCache is:

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

I create the fakes in the constructor of the test (using 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();
}    

The error from running the test is:

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.Func1[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.>

I get the fact that faking a static method is not on. I'm looking for solutions.

Do I need to pressure library authors to not use extension methods? (Facetious question)

Cheers


Solution

  • As you have correctly noted, extensions are static methods, and static methods can't be faked.

    Extension methods are often just wrappers to simplify operations on the type they extend; it appears to be the case here. The GetOrAddAsync extension method you're calling ends up calling the IAppCache.GetOrAddAsync method. So you should fake that method instead.

    A.CallTo(() => _appCache.GetOrAddAsync(cacheKey, A<Func<ICacheEntry, Task<SelectList>>>.Ignored))
            .Returns(new SelectList(Enumerable.Empty<SelectListItem>().ToList()));
    
    

    It's not very convenient, because it means you need to know what the extension method does, but there's no way around it (short of creating an abstraction layer around the library, but LazyCache is already an abstraction around Microsoft.Extensions.Caching.Memory...)

    (btw, you don't need Task.FromResult; the Returns method has an overload that accepts a T when you configure a method returning a Task<T>)


    Also, if you're going to return an empty sequence anyway, you don't need to configure the method at all. The default behavior of FakeItEasy will be to return a dummy IEnumerable<SelectListItem> which is empty.