So I'm trying to test caching behaviour in an app that's using Akavache. My test looks like this:
using Akavache;
using Microsoft.Reactive.Testing;
using Moq;
using NUnit.Framework;
using ReactiveUI.Testing;
using System;
using System.Threading.Tasks;
[TestFixture]
public class CacheFixture
{
[Test]
public async Task CachingTest()
{
var scheduler = new TestScheduler();
// replacing the TestScheduler with the scheduler below works
// var scheduler = CurrentThreadScheduler.Instance;
var cache = new InMemoryBlobCache(scheduler);
var someApi = new Mock<ISomeApi>();
someApi.Setup(s => s.GetSomeStrings())
.Returns(Task.FromResult("helloworld")).Verifiable();
var apiWrapper = new SomeApiWrapper(someApi.Object, cache,
TimeSpan.FromSeconds(10));
var string1 = await apiWrapper.GetSomeStrings();
someApi.Verify(s => s.GetSomeStrings(), Times.Once());
StringAssert.AreEqualIgnoringCase("helloworld", string1);
scheduler.AdvanceToMs(5000);
// without the TestScheduler, I'd have to 'wait' here
// await Task.Delay(5000);
var string2 = await apiWrapper.GetSomeStrings();
someApi.Verify(s => s.GetSomeStrings(), Times.Once());
StringAssert.AreEqualIgnoringCase("helloworld", string2);
}
}
The SomeApiWrapper
uses an internal api (mocked with new Mock<ISomeApi>()
) that - for simplicity's sake - just returns a string. The problem now is that the second string is never returned. The SomeApiWrapper
class that handles the caching looks like this:
using Akavache;
using System;
using System.Reactive.Linq;
using System.Threading.Tasks;
public class SomeApiWrapper
{
private IBlobCache Cache;
private ISomeApi Api;
private TimeSpan Timeout;
public SomeApiWrapper(ISomeApi api, IBlobCache cache, TimeSpan cacheTimeout)
{
Cache = cache;
Api = api;
Timeout = cacheTimeout;
}
public async Task<string> GetSomeStrings()
{
var key = "somestrings";
var cachedStrings = Cache.GetOrFetchObject(key, DoGetStrings,
Cache.Scheduler.Now.Add(Timeout));
// this is the last step, after this it just keeps running
// but never returns - but only for the 2nd call
return await cachedStrings.FirstOrDefaultAsync();
}
private async Task<string> DoGetStrings()
{
return await Api.GetSomeStrings();
}
}
Debugging only leads me to the line return await cachedStrings.FirstOrDefaultAsync();
- and it never finishes after that.
When I replace the TestScheduler
with the standard (CurrentThreadScheduler.Instance
) and the scheduler.AdvanceToMs(5000)
with await Task.Delay(5000)
, everything works as expected but I don't want unit tests running for multiple seconds.
A similar test, where the TestScheduler
is advanced past the cache timeout also succeeds. It's just this scenario, where the cache entry should not expire in between the two method calls.
Is there something I'm doing wrong in the way I'm using TestScheduler
?
This is a fairly common problem when bouncing between the Task
and the IObservable
paradigms. It is further exacerbated by trying to wait before moving forward in the tests.
The key problem is that you are blocking* here
return await cachedStrings.FirstOrDefaultAsync();
I say blocking in the sense that the code can not continue to process until this statement yields.
On the first run the cache can not find the key, so it executes your DoGetStrings
. The issue surfaces on the second run, where the cache is populated. This time (I guess) the fetching of the cached data is scheduled. You need to invoke the request, observe the sequence, then pump the scheduler.
The corrected code is here (but requires some API changes)
[TestFixture]
public class CacheFixture
{
[Test]
public async Task CachingTest()
{
var testScheduler = new TestScheduler();
var cache = new InMemoryBlobCache(testScheduler);
var cacheTimeout = TimeSpan.FromSeconds(10);
var someApi = new Mock<ISomeApi>();
someApi.Setup(s => s.GetSomeStrings())
.Returns(Task.FromResult("helloworld")).Verifiable();
var apiWrapper = new SomeApiWrapper(someApi.Object, cache, cacheTimeout);
var string1 = await apiWrapper.GetSomeStrings();
someApi.Verify(s => s.GetSomeStrings(), Times.Once());
StringAssert.AreEqualIgnoringCase("helloworld", string1);
testScheduler.AdvanceToMs(5000);
var observer = testScheduler.CreateObserver<string>();
apiWrapper.GetSomeStrings().Subscribe(observer);
testScheduler.AdvanceByMs(cacheTimeout.TotalMilliseconds);
someApi.Verify(s => s.GetSomeStrings(), Times.Once());
StringAssert.AreEqualIgnoringCase("helloworld", observer.Messages[0].Value.Value);
}
}
public interface ISomeApi
{
Task<string> GetSomeStrings();
}
public class SomeApiWrapper
{
private IBlobCache Cache;
private ISomeApi Api;
private TimeSpan Timeout;
public SomeApiWrapper(ISomeApi api, IBlobCache cache, TimeSpan cacheTimeout)
{
Cache = cache;
Api = api;
Timeout = cacheTimeout;
}
public IObservable<string> GetSomeStrings()
{
var key = "somestrings";
var cachedStrings = Cache.GetOrFetchObject(key, DoGetStrings,
Cache.Scheduler.Now.Add(Timeout));
//Return an observerable here instead of "blocking" with a task. -LC
return cachedStrings.Take(1);
}
private async Task<string> DoGetStrings()
{
return await Api.GetSomeStrings();
}
}
This code is green and runs sub-second.