【问题标题】:Unit testing that events are raised in C# (in order)在 C# 中引发事件的单元测试(按顺序)
【发布时间】:2010-09-19 22:10:52
【问题描述】:

我有一些引发PropertyChanged 事件的代码,我希望能够对这些事件进行正确引发的单元测试。

引发事件的代码是这样的

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

在我的单元测试中,我从以下代码中得到了一个不错的绿色测试,它使用了委托:

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

我的事件测试失败 - 它捕获的事件是 MyOtherProperty 的事件。

我很确定事件会触发,我的 UI 会像它一样做出反应,但我的委托只捕获最后一个触发的事件。

所以我想知道:
1. 我测试事件的方法正确吗?
2. 我提出 chained 事件的方法是否正确?

【问题讨论】:

    标签: c# unit-testing events


    【解决方案1】:

    您所做的一切都是正确的,前提是您希望您的测试询问“引发的最后一个事件是什么?”

    您的代码按此顺序触发这两个事件

    • 属性已更改(...“我的属性”...)
    • 属性已更改(...“MyOtherProperty”...)

    这是否“正确”取决于这些事件的目的。

    如果您想测试引发事件的数量以及引发事件的顺序,您可以轻松扩展现有测试:

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

    【讨论】:

    • 短版:myClass.PropertyChanged += (object sender, e) => receivedEvents.Add(e.PropertyName);
    【解决方案2】:

    如果您正在执行 TDD,那么事件测试可能会开始生成 很多 重复代码。我编写了一个事件监视器,它可以为这些情况提供更简洁的单元测试编写方法。

    var publisher = new PropertyChangedEventPublisher();
    
    Action test = () =>
    {
        publisher.X = 1;
        publisher.Y = 2;
    };
    
    var expectedSequence = new[] { "X", "Y" };
    
    EventMonitor.Assert(test, publisher, expectedSequence);
    

    请参阅我对以下内容的回答了解更多详情。

    Unit testing that an event is raised in C#, using reflection

    【讨论】:

    • 第二个链接失效了。
    【解决方案3】:

    这是非常古老的,甚至可能都不会被阅读,但是我创建了一个 INPC Tracer 类,它允许使用一些很酷的新 .net 功能:

    [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

    【讨论】:

    • 漂亮 - 您应该考虑将其打包并发布到 nuget.org
    • 干得好!我真的很喜欢流畅的 API。我自己也做过类似的事情(github.com/f-tischler/EventTesting),但我认为你的方法更加简洁。
    【解决方案4】:

    以下是 Andrew 的代码稍作改动,它不仅记录引发事件的序列,还计算特定事件被调用的次数。虽然它基于他的代码,但我发现它在我的测试中更有用。

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

    【讨论】:

      【解决方案5】:

      根据这篇文章,我创建了这个简单的断言助手:

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

      有了这个方法助手,测试变得非常简单。

      [TestMethod()]
      public void Event_UserName_PropertyChangedWillBeFired()
      {
          var user = new User();
          AssertPropertyChanged(user, (x) => x.UserName = "Bob", "UserName");
      }
      

      【讨论】:

        【解决方案6】:

        不要为每个成员编写测试 - 这是很多工作

        (也许这个解决方案并不适合所有情况 - 但它显示了一种可能的方法。您可能需要根据您的用例对其进行调整)

        可以在库中使用反射来测试您的成员是否都正确响应您的属性更改事件:

        • 在 setter 访问时引发 PropertyChanged 事件
        • 事件引发正确(属性名称等于引发事件的参数)

        以下代码可用作库并展示如何测试以下泛型类

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

        你的类的测试现在可以写成。 (也许您想将测试拆分为“事件存在”和“使用正确名称引发的事件” - 您可以自己执行此操作)

        [TestMethod]
        public void EveryWriteablePropertyImplementsINotifyPropertyChangedCorrect()
        {
            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");
                    }
                }
        }
        

        【讨论】:

        • 我试过了,但如果我的属性设置器有一个像“if(value == _myValue) return”这样的保护语句,我的所有方法都这样做,那么上面的方法就行不通了,除非我遗漏了什么。我最近从 C++ 转向 C#。
        【解决方案7】:

        我在这里做了一个扩展:

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

        【讨论】:

          猜你喜欢
          • 2011-05-04
          • 1970-01-01
          • 1970-01-01
          • 2014-07-13
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多