unit testing一个事件在C#中引发
我有一些引发PropertyChanged
事件的代码,我希望能够unit testing事件正在提出正确。
引发事件的代码就像
public class MyClass : INotifyPropertyChanged { public event PropertyChangedEventHandler PropertyChanged; protected void NotifyPropertyChanged(String info) { if (PropertyChanged != null) { PropertyChanged(this, new PropertyChangedEventArgs(info)); } } public string MyProperty { set { if (_myProperty != value) { _myProperty = value; NotifyPropertyChanged("MyProperty"); } } } }
我在我的unit testing中使用委托,从下面的代码中得到一个很好的绿色testing:
[TestMethod] public void Test_ThatMyEventIsRaised() { string actual = null; MyClass myClass = new MyClass(); myClass.PropertyChanged += delegate(object sender, PropertyChangedEventArgs e) { actual = e.PropertyName; }; myClass.MyProperty = "testing"; Assert.IsNotNull(actual); Assert.AreEqual("MyProperty", actual); }
但是,如果我尝试将属性的设置链接在一起,如下所示:
public string MyProperty { set { if (_myProperty != value) { _myProperty = value; NotifyPropertyChanged("MyProperty"); MyOtherProperty = "SomeValue"; } } } public string MyOtherProperty { set { if (_myOtherProperty != value) { _myOtherProperty = value; NotifyPropertyChanged("MyOtherProperty"); } } }
我对事件的testing失败 – 它捕获的事件是MyOtherProperty事件。
我很确定事件触发,我的用户界面反应就像它,但我的委托只捕获最后一个事件触发。
所以我想知道:
我的testing方法是否正确?
我的提炼链式事件的方法是否正确?
你所做的一切都是正确的,只要你想要testing就可以问“最后发生了什么事?”
你的代码按这个顺序触发这两个事件
- Property Changed(…“My Property”…)
- Property Changed(…“MyOtherProperty”…)
这是否“正确”取决于这些事件的目的。
如果您想testing引发的事件数量以及提出的顺序,您可以轻松地扩展现有的testing:
[TestMethod] public void Test_ThatMyEventIsRaised() { List<string> receivedEvents = new List<string>(); MyClass myClass = new MyClass(); myClass.PropertyChanged += delegate(object sender, PropertyChangedEventArgs e) { receivedEvents.Add(e.PropertyName); }; myClass.MyProperty = "testing"; Assert.AreEqual(2, receivedEvents.Count); Assert.AreEqual("MyProperty", receivedEvents[0]); Assert.AreEqual("MyOtherProperty", receivedEvents[1]); }
如果你正在做TDD,那么事件testing就会开始产生大量的重复代码。 我写了一个事件监视器,为这些情况提供了更简洁的unit testing编写方法。
var publisher = new PropertyChangedEventPublisher(); Action test = () => { publisher.X = 1; publisher.Y = 2; }; var expectedSequence = new[] { "X", "Y" }; EventMonitor.Assert(test, publisher, expectedSequence);
有关更多详细信息,请参阅以下答案。
使用reflectionunit testing在C#中引发事件
或者我发布的一系列博客文章:
http://gojisoft.com/blog/2010/04/22/event-sequence-unit-testing-part-1/
下面是一个稍微改变的安德鲁的代码,而不是只logging引发事件的序列,而是计算一个特定事件被调用的次数。 虽然它是基于他的代码,我发现它在我的testing中更有用。
[TestMethod] public void Test_ThatMyEventIsRaised() { Dictionary<string, int> receivedEvents = new Dictionary<string, int>(); MyClass myClass = new MyClass(); myClass.PropertyChanged += delegate(object sender, PropertyChangedEventArgs e) { if (receivedEvents.ContainsKey(e.PropertyName)) receivedEvents[e.PropertyName]++; else receivedEvents.Add(e.PropertyName, 1); }; myClass.MyProperty = "testing"; Assert.IsTrue(receivedEvents.ContainsKey("MyProperty")); Assert.AreEqual(1, receivedEvents["MyProperty"]); Assert.IsTrue(receivedEvents.ContainsKey("MyOtherProperty")); Assert.AreEqual(1, receivedEvents["MyOtherProperty"]); }
这是非常古老的,甚至可能不会被阅读,但有一些很酷的新的.NETfunction,我创build了一个INPC Tracer类,允许:
[Test] public void Test_Notify_Property_Changed_Fired() { var p = new Project(); var tracer = new INCPTracer(); // One event tracer.With(p).CheckThat(() => p.Active = true).RaisedEvent(() => p.Active); // Two events in exact order tracer.With(p).CheckThat(() => p.Path = "test").RaisedEvent(() => p.Path).RaisedEvent(() => p.Active); }
请参阅: https : //gist.github.com/Seikilos/6224204
基于这篇文章,我创build了这个简单的断言帮手:
private void AssertPropertyChanged<T>(T instance, Action<T> actionPropertySetter, string expectedPropertyName) where T : INotifyPropertyChanged { string actual = null; instance.PropertyChanged += delegate (object sender, PropertyChangedEventArgs e) { actual = e.PropertyName; }; actionPropertySetter.Invoke(instance); Assert.IsNotNull(actual); Assert.AreEqual(propertyName, actual); }
有了这个方法的帮手,testing变得非常简单。
[TestMethod()] public void Event_UserName_PropertyChangedWillBeFired() { var user = new User(); AssertPropertyChanged(user, (x) => x.UserName = "Bob", "UserName"); }
不要为每个成员写一个testing – 这是很多工作
(也许这个解决scheme并不适合所有的情况 – 但是它显示了一个可能的方法,你可能需要根据你的使用情况来调整它)
可以在库中使用reflection来testing您的成员是否都正确地响应您的属性更改的事件:
- 在setter访问上引发PropertyChanged事件
- 事件提出正确(属性名称等于事件提出的论据)
下面的代码可以用一个库显示如何testinggernic类
using System.ComponentModel; using System.Linq; /// <summary> /// Check if every property respons to INotifyPropertyChanged with the correct property name /// </summary> public static class NotificationTester { public static object GetPropertyValue(object src, string propName) { return src.GetType().GetProperty(propName).GetValue(src, null); } public static bool Verify<T>(T inputClass) where T : INotifyPropertyChanged { var properties = inputClass.GetType().GetProperties().Where(x => x.CanWrite); var index = 0; var matchedName = 0; inputClass.PropertyChanged += (o, e) => { if (properties.ElementAt(index).Name == e.PropertyName) { matchedName++; } index++; }; foreach (var item in properties) { // use setter of property item.SetValue(inputClass, GetPropertyValue(inputClass, item.Name)); } return matchedName == properties.Count(); } }
现在你的class级的考试可以写成。 (也许你想把testing分成“事件在那里”和“事件提出正确的名称” – 你可以自己做这个)
[TestMethod] public void EveryWriteablePropertyImpementsINotifyPropertyChangedCorrect() { var viewModel = new TestMyClassWithINotifyPropertyChangedInterface(); Assert.AreEqual(true, NotificationTester.Verify(viewModel)); }
类
using System.ComponentModel; public class TestMyClassWithINotifyPropertyChangedInterface : INotifyPropertyChanged { public event PropertyChangedEventHandler PropertyChanged; protected void NotifyPropertyChanged(string name) { if (PropertyChanged != null) { PropertyChanged(this, new PropertyChangedEventArgs(name)); } } private int id; public int Id { get { return id; } set { id = value; NotifyPropertyChanged("Id"); } } }
我在这里做了一个扩展:
public static class NotifyPropertyChangedExtensions { private static bool _isFired = false; private static string _propertyName; public static void NotifyPropertyChangedVerificationSettingUp(this INotifyPropertyChanged notifyPropertyChanged, string propertyName) { _isFired = false; _propertyName = propertyName; notifyPropertyChanged.PropertyChanged += OnPropertyChanged; } private static void OnPropertyChanged(object sender, PropertyChangedEventArgs e) { if (e.PropertyName == _propertyName) { _isFired = true; } } public static bool IsNotifyPropertyChangedFired(this INotifyPropertyChanged notifyPropertyChanged) { _propertyName = null; notifyPropertyChanged.PropertyChanged -= OnPropertyChanged; return _isFired; } }
有这样的用法:
[Fact] public void FilesRenameViewModel_Rename_Apply_Execute_Verify_NotifyPropertyChanged_If_Succeeded_Through_Extension_Test() { // Arrange _filesViewModel.FolderPath = ConstFolderFakeName; _filesViewModel.OldNameToReplace = "Testing"; //After the command's execution OnPropertyChanged for _filesViewModel.AllFilesFiltered should be raised _filesViewModel.NotifyPropertyChangedVerificationSettingUp(nameof(_filesViewModel.AllFilesFiltered)); //Act _filesViewModel.ApplyRenamingCommand.Execute(null); // Assert Assert.True(_filesViewModel.IsNotifyPropertyChangedFired()); }