NSubstitute Received() verifies different function in substite.
jyrijh opened this issue · 2 comments
Code is calling _cache.SetStringAsync, but _cache.Received(1).SetStringAsync fails with error
Message:
NSubstitute.Exceptions.ReceivedCallsException : Expected to receive exactly 1 call matching:
SetAsync("SearchBulletin{"Id":null,"ProgramId":null,"Title":null,"Creator":null,"CurrentUser":null,"IsAdmin":false,"StartTime":null,"EndTime":null}", Byte[], any DistributedCacheEntryOptions, System.Threading.CancellationToken)
Actually received no matching calls.
Received 1 non-matching call (non-matching arguments indicated with '*' characters):
SetAsync("SearchBulletin{"Id":null,"ProgramId":null,"Title":null,"Creator":null,"CurrentUser":null,"IsAdmin":false,"StartTime":null,"EndTime":null}", *Byte[]*, DistributedCacheEntryOptions, System.Threading.CancellationToken)
So why am I seeing SetAsync, when I am testing for SetStringAsync, and calling SetStringAsync.
SetStringAsync internally calls SetAsync, but that is call from DistributedCacheExtensions
public static Task SetStringAsync(this IDistributedCache cache, string key, string value, DistributedCacheEntryOptions options, CancellationToken token = default(CancellationToken))
{
ThrowHelper.ThrowIfNull(key);
ThrowHelper.ThrowIfNull(value);
return cache.SetAsync(key, Encoding.UTF8.GetBytes(value), options, token);
}
Test
private readonly IUnderlyingBulletinAdministrationRepository _repository;
private readonly IDistributedCache _cache;
private readonly CachedAdminstrationRepository _sut;
ILogger<CachedAdminstrationRepository> logger = Substitute.For<ILogger<CachedAdminstrationRepository>>();
CancellationToken ct = CancellationToken.None;
DistributedCacheEntryOptions cacheOptions = new DistributedCacheEntryOptions { SlidingExpiration = TimeSpan.FromMinutes(1) };
public CachedAdminRepositoryBulletinSearchTests()
{
_repository = Substitute.For<IUnderlyingBulletinAdministrationRepository>();
_cache = Substitute.For<IDistributedCache>();
_sut = new CachedAdminstrationRepository(_repository, _cache, logger);
}
[Fact]
public async Task BulletinSearch_GetsListOfBulletinsFromCache()
{
// Arrange
int id = 1;
BulletinResponse expected = new BulletinResponse { Id = id, Otsikko = "test" };
_repository.SearchBulletinAsync(Arg.Any<SearchBulletin>(), ct).Returns([expected]);
string cacheKey = BulletinCache.Keys.SearchBulletin(new SearchBulletin());
string cacheValue = JsonSerializer.Serialize<List<BulletinResponse>>([expected]);
// Act
_ = await _sut.SearchBulletinAsync(new SearchBulletin(), ct);
// Assert
await _cache.Received(1).GetStringAsync(cacheKey, ct);
await _cache.Received(1).SetStringAsync(cacheKey, cacheValue, Arg.Any<DistributedCacheEntryOptions>(), ct);
}
Cache implementation
public async Task<List<BulletinResponse>> SearchBulletinAsync(SearchBulletin search, CancellationToken ct)
{
var key = BulletinCache.Keys.SearchBulletin(search);
return await GetOrSetCacheAsync(
key,
token => _repository.SearchBulletinAsync(search, token),
ct);
}
private async Task<T> GetOrSetCacheAsync<T>(string key, Func<CancellationToken, Task<T>> getActual, CancellationToken ct)
{
var cacheResult = await _cache.GetStringAsync(key, ct);
if (string.IsNullOrEmpty(cacheResult))
{
_logger.LogInformation("Cache miss for key {key}", key);
var result = await getActual(ct);
await _cache.SetStringAsync(
key,
JsonSerializer.Serialize(result),
new DistributedCacheEntryOptions { SlidingExpiration = TimeSpan.FromMinutes(_expirationMinutes) },
ct);
return result;
}
else
{
_logger.LogInformation("Cache hit for key {key}", key);
return JsonSerializer.Deserialize<T>(cacheResult)!;
}
}
Hi @jyrijh
Thanks for raising the issue.
That happens because you are checking received calls to a static method, SetStringAsync
in your case.
NSubstitute works with virtual members only and doesn't "see" static methods. That's why it picked the next virtual call which is SetAsync
in your case. Please see the docs that explain that in details.
We strongly recommend using NSubstitute.Analyzers to detect these cases.
Let us know if it doesn't help.
Thanks, now this makes sense. Lesson learned. Don't try to check if an extension method is called 😖. Unless that is what you want to happen