使用可观察模式测试回调
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
来补救。
我想用 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
来补救。