2

所以我正在尝试在使用 Akavache 的应用程序中测试缓存行为。我的测试如下所示:

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

SomeApiWrapper使用内部 api(用模拟new Mock<ISomeApi>()) - 为简单起见 - 只返回一个字符串。现在的问题是第二个字符串永远不会返回。处理缓存的SomeApiWrapper类如下所示:

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

调试只会把我引向这条线return await cachedStrings.FirstOrDefaultAsync();——之后它就永远不会结束。

当我用TestScheduler标准 ( CurrentThreadScheduler.Instance) 和scheduler.AdvanceToMs(5000)with替换 时await Task.Delay(5000),一切都按预期工作,但我不希望单元测试运行多秒钟。

一个类似的测试,其中TestScheduler超过缓存超时也成功。只是这种情况,缓存条目不应该在两个方法调用之间过期。

我使用的方式有什么问题TestScheduler吗?

4

1 回答 1

8

Task在范式和范式之间来回切换时,这是一个相当普遍的问题IObservable。试图在测试继续前等待会进一步加剧这种情况。

关键问题是你在这里阻塞*

return await cachedStrings.FirstOrDefaultAsync();

我所说的阻塞是指代码在此语句产生之前无法继续处理。

在第一次运行时缓存找不到键,所以它执行你的DoGetStrings. 问题在第二次运行时浮出水面,其中填充了缓存。这一次(我猜)缓存数据的获取被安排了。您需要调用请求,观察顺序,然后抽出调度程序。

更正的代码在这里(但需要一些 API 更改)

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

这段代码是绿色的,运行亚秒级。

于 2016-02-11T04:24:11.060 回答