使用可观察模式测试回调

Testing callback with observable pattern

我想用 NUnit 为我们的 wpf 应用程序编写一些单元测试。 应用程序使用观察者模式在后台下载一些 System.Net.WebClient 的数据。

这是一个例子:

Download.cs

public class Download : IObservable<string>
{
    private string url { get; }
    private List<IObserver<string>> observers = new List<IObserver<string>>();
    private bool closed = false;
    private string data = null;

    public Download(string url)
    {
        this.url = url;

        startDownload();
    }

    public IDisposable Subscribe(IObserver<string> observer)
    {
        if (!observers.Contains(observer))
        {
            if (!closed)
            {
                observers.Add(observer);
            }
            else
            {
                sendAndComplete(observer);
            }

        }

        return new Unsubscriber(observer, observers);
    }

    private void startDownload()
    {
        WebClient client = new WebClient();
        client.DownloadStringCompleted += new DownloadStringCompletedEventHandler((object sender, DownloadStringCompletedEventArgs e) => {
            if (e.Error != null)
            {
                data = e.Result;
            }

            closed = true;
            sendAndComplete();
        });

        client.DownloadStringAsync(new Uri(url));
    }

    private void sendAndComplete()
    {
        foreach (var observer in observers)
        {
            sendAndComplete(observer);
        }

        observers.Clear();
    }

    private void sendAndComplete(IObserver<string> observer)
    {
        if (data != null)
        {
            observer.OnNext(data);
        }
        else
        {
            observer.OnError(new Exception("Download failed!"));
        }

        observer.OnCompleted();
    }

    private class Unsubscriber : IDisposable
    {
        private IObserver<string> _observer { get; }
        private List<IObserver<string>> _observers { get; }

        public Unsubscriber(IObserver<string> _observer, List<IObserver<string>> _observers)
        {
            this._observer = _observer;
            this._observers = _observers;
        }

        public void Dispose()
        {
            if (_observer != null && _observers.Contains(_observer))
            {
                _observers.Remove(_observer);
            }
        }
    }
}

DownloadInspector.cs

public class DownloadInspector : IObserver<string>
{
    private Action<string> onSuccessAction { get; }
    private Action<Exception> onErrorAction { get; }
    private Action onCompleteAction { get; }

    public DownloadInspector(Action<string> onSuccessAction, Action<Exception> onErrorAction, Action onCompleteAction)
    {
        this.onSuccessAction = onSuccessAction;
        this.onErrorAction = onErrorAction;
        this.onCompleteAction = onCompleteAction;
    }

    public void OnCompleted()
    {
        onCompleteAction.Invoke();
    }

    public void OnError(Exception error)
    {
        onErrorAction.Invoke(error);
    }

    public void OnNext(string value)
    {
        onSuccessAction.Invoke(value);
    }
}

示例(用法)

Download download = new Download("http://whosebug.com");
DownloadInspector inspector = new DownloadInspector(
    (string data) =>
    {
        Debug.WriteLine("HANDLE DATA");
    },
    (Exception error) =>
    {
        Debug.WriteLine("HANDLE ERROR");
    },
    () =>
    {
        Debug.WriteLine("HANDLE COMPLETE");
    }
    );

我对 c# 还是个新手,不太熟悉该语言的异步编程。我知道 await 和 async 关键字,并且知道它们与 NUnit 一起使用,但当前构造不使用 this 关键字。

你能帮我为这个案例创建一个单元测试吗? change/remove观察者模式没问题

Download class 的构造函数开始下载,这意味着在下载开始之前我无法订阅观察者。那是一种竞争条件。观察者有可能(尽管不太可能)在订阅之前收到通知。

public Download(string url)
{
    this.url = url;
    startDownload();
}

但我可以继续进行测试,因为我会在此之前订阅一个观察器。如果可以的话,我建议不要那样做。允许调用者一步构建 class,然后通过方法调用开始下载。

我也不得不改变这个方法。我认为测试错误是最简单的第一步,但如果有 没有 错误,则需要执行 data = e.Result,如果有错误则不需要。

private void StartDownload()
{
    WebClient client = new WebClient();
    client.DownloadStringCompleted += new DownloadStringCompletedEventHandler((object sender, DownloadStringCompletedEventArgs e) =>
    {
        if (e.Error == null) // <== because of this
        {
            data = e.Result;
        }

        closed = true;
        sendAndComplete();
    });    

    client.DownloadStringAsync(new Uri(url));
}

我没有看到的是 WebClient.DownloadStringAsync 实际上并不是异步的。它不 return 一个 Task。它只需要一个回调。这意味着除了等待它通知观察者下载已完成之外,没有确定的方法可以知道它是否已完成。

我的 NUnit 测试 运行ner 不是 运行ning,所以我使用了 MsTest。这是同一件事。

基本方法是我正在创建一些标志,检查员通过设置标志来响应通知。这样我就可以看到发出了哪些通知。

最后一个问题是因为DownloadStringComplete是一个回调,测试在Assert之前就退出了。这意味着它总是会过去的。所以为了修复它,我不得不做一些我以前从未见过的事情,我发现 here:

[TestMethod]
public void download_raises_error_notification()
{
    var success = false;
    bool error = false;
    bool complete = false;
    var pause = new ManualResetEvent(false);

    var download = new Download("http://NoSuchUrlAnywhere.com");
    var inspector = new DownloadInspector(
        onSuccessAction: s => success = true,
        onCompleteAction: () =>
        {
            complete = true;
            pause.Set();
        },
        onErrorAction: s => error = true
        );

    download.Subscribe(inspector);

    // allow 500ms for the download to fail. This is a race condition.
    pause.WaitOne(500);

    Assert.IsTrue(error,"onErrorAction was not called.");
}

这在技术上是一个集成测试,因为它必须实际尝试下载才能 运行。这可以通过嘲笑 WebClient 来补救。