Тестирование обратного вызова с наблюдаемым шаблоном - PullRequest
0 голосов
/ 07 мая 2019

Я хочу написать несколько тестов с 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://stackoverflow.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, но текущая конструкция не использует эти ключевые слова.

Можете ли вы помочь мне создать модульный тест для этого случая?Можно изменить / удалить шаблон наблюдателя.

1 Ответ

2 голосов
/ 07 мая 2019

Конструктор для класса Download запускает загрузку, а это значит, что я не могу подписаться на наблюдателя, пока не начнется загрузка.Это состояние гонки.Возможно (хотя и маловероятно), что наблюдатели будут уведомлены, прежде чем они могут быть подписаны.

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

Но я могу пойти дальше и проверить, потому что я подписываюсь на наблюдателя, прежде чем это произойдет.Если вы можете, я бы рекомендовал не делать этого.Разрешить вызывающей стороне создать класс за один шаг, а затем начать загрузку с помощью вызова метода.

Мне также пришлось изменить этот метод.Я полагал, что тестирование на ошибку было бы самым простым первым шагом, но он должен сделать data = e.Result, если есть ошибка no , а не если есть ошибка.

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 на самом деле не асинхронный.Он не возвращает Task.Это просто требует обратного вызова.Это означает, что нет точного способа узнать, сделано ли это, кроме как ждать, пока он уведомит наблюдателя о завершении загрузки.

Мой тестовый прогон NUnit не работал, поэтому я использовал MsTest.Это то же самое.

Основной подход заключается в том, что я создаю некоторые флаги, а инспектор отвечает на уведомления, устанавливая флаги.Таким образом, я могу видеть, какие уведомления были получены.

Последняя проблема заключается в том, что, поскольку DownloadStringComplete является обратным вызовом, тест завершается до Assert.Это означает, что это всегда пройдет.Поэтому, чтобы исправить это, я должен был сделать что-то, чего я никогда раньше не видел, и нашел здесь :

[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.

...