2

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?

germi
  • 4,628
  • 1
  • 21
  • 38
  • 1
    There's several things in flight here. NUnit in particular will inject a context in some situations. What is the value of `SynchronizationContext.Current` immediately before the `var string2` line? – Stephen Cleary Feb 08 '16 at 18:24
  • @StephenCleary The `SynchronizationContext.Current` is `null` both times (i. e. before the `string1` line and before the `string2` line). – germi Feb 08 '16 at 21:18
  • I just tried setting the `SynchronizationContext` manually by calling `SynchronizationContext.SetSynchronizationContext(new SynchronizationContext());` in the setup, but that didn't help. – germi Feb 08 '16 at 21:36

1 Answers1

8

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.

Lee Campbell
  • 10,631
  • 1
  • 34
  • 29
  • Thanks, this does indeed work! I guess I'll have to read up on how to properly handle `Observables` in conjunction with `Tasks` but this put me in the right direction. – germi Feb 11 '16 at 18:37
  • In my experience, I have found that it is best to try to avoid mixing the two paradigms. While tasks are "natively" supported, they are eagerly evaluated in contrast to Rx being lazy by default, and their continuation model (async/await) makes it easy to make these mistakes. – Lee Campbell Feb 12 '16 at 02:21