Unit-Test, dass Ereignisse in C # ausgelöst werden (in der Reihenfolge)

159

Ich habe einen Code, der PropertyChangedEreignisse auslöst , und ich möchte einen Unit-Test durchführen können, um sicherzustellen, dass die Ereignisse korrekt ausgelöst werden.

Der Code, der die Ereignisse auslöst, ist wie folgt

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

Ich erhalte einen schönen grünen Test aus dem folgenden Code in meinen Komponententests, der Delegaten verwendet:

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

Wenn ich dann jedoch versuche, die Einstellung der Eigenschaften wie folgt zu verketten:

public string MyProperty
{
    set
    {
        if (_myProperty != value)
        {
            _myProperty = value;
            NotifyPropertyChanged("MyProperty");
            MyOtherProperty = "SomeValue";
        }
    }
}

public string MyOtherProperty
{
    set
    {
        if (_myOtherProperty != value)
        {
            _myOtherProperty = value;
            NotifyPropertyChanged("MyOtherProperty");
        }
    }
}

Mein Test für das Ereignis schlägt fehl - das Ereignis, das erfasst wird, ist das Ereignis für die MyOtherProperty.

Ich bin mir ziemlich sicher, dass das Ereignis ausgelöst wird, meine Benutzeroberfläche reagiert wie es ist, aber mein Delegat erfasst nur das letzte Ereignis, das ausgelöst wird.

Ich frage mich also:
1. Ist meine Methode zum Testen von Ereignissen korrekt?
2. Ist meine Methode zum Auslösen verketteter Ereignisse korrekt?

David Hall
quelle

Antworten:

189

Alles, was Sie getan haben, ist korrekt, vorausgesetzt, Sie möchten, dass Ihr Test fragt: "Was ist das letzte Ereignis, das ausgelöst wurde?"

Ihr Code löst diese beiden Ereignisse in dieser Reihenfolge aus

  • Eigenschaft geändert (... "Mein Eigentum" ...)
  • Eigenschaft geändert (... "MyOtherProperty" ...)

Ob dies "richtig" ist oder nicht, hängt vom Zweck dieser Ereignisse ab.

Wenn Sie die Anzahl der Ereignisse, die ausgelöst werden, und die Reihenfolge, in der sie ausgelöst werden, testen möchten, können Sie Ihren vorhandenen Test problemlos erweitern:

[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]);
}
Andrew Stapleton
quelle
13
Kürzere Version: myClass.PropertyChanged + = (Objektabsender, e) => receiveEvents.Add (e.PropertyName);
ShloEmi
22

Wenn Sie TDD ausführen, können durch Ereignistests viele sich wiederholende Codes generiert werden. Ich habe einen Ereignismonitor geschrieben, der einen viel saubereren Ansatz für das Schreiben von Komponententests für diese Situationen ermöglicht.

var publisher = new PropertyChangedEventPublisher();

Action test = () =>
{
    publisher.X = 1;
    publisher.Y = 2;
};

var expectedSequence = new[] { "X", "Y" };

EventMonitor.Assert(test, publisher, expectedSequence);

Bitte lesen Sie meine Antwort auf das Folgende für weitere Details.

Unit-Test, dass ein Ereignis in C # mithilfe von Reflektion ausgelöst wird

Tim Lloyd
quelle
3
Der zweite Link ist ausgefallen.
Lennart
10

Dies ist sehr alt und wird wahrscheinlich nicht einmal gelesen, aber mit einigen coolen neuen .net-Funktionen habe ich eine INPC Tracer-Klasse erstellt, die Folgendes ermöglicht:

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

Siehe Inhalt: https://gist.github.com/Seikilos/6224204

Samuel
quelle
Schön - Sie sollten in Betracht ziehen, es zu verpacken und auf nuget.org zu veröffentlichen
Simon Ejsing
1
Gute Arbeit! Ich mag die fließende API wirklich. Ich habe selbst etwas Ähnliches gemacht ( github.com/f-tischler/EventTesting ), aber ich denke, Ihr Ansatz ist noch prägnanter.
Florian Tischler
6

Unten finden Sie einen leicht geänderten Andrew-Code, der nicht nur die Abfolge der ausgelösten Ereignisse protokolliert, sondern auch zählt, wie oft ein bestimmtes Ereignis aufgerufen wurde. Obwohl es auf seinem Code basiert, finde ich es in meinen Tests nützlicher.

[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"]);
}
Damir Arh
quelle
1

Basierend auf diesem Artikel habe ich diesen einfachen Assertions-Helfer erstellt:

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

Mit diesem Methodenhelfer wird der Test wirklich einfach.

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

Schreiben Sie nicht für jedes Mitglied einen Test - das ist viel Arbeit

(Vielleicht ist diese Lösung nicht für jede Situation perfekt - aber sie zeigt einen möglichen Weg. Möglicherweise müssen Sie sie für Ihren Anwendungsfall anpassen.)

Mithilfe der Reflektion in einer Bibliothek können Sie testen, ob Ihre Mitglieder alle korrekt auf das Ereignis reagieren, bei dem sich Ihr Eigentum geändert hat:

  • Das PropertyChanged-Ereignis wird beim Setter-Zugriff ausgelöst
  • Ereignis wird korrekt ausgelöst (Name der Eigenschaft entspricht Argument des ausgelösten Ereignisses)

Der folgende Code kann als Bibliothek verwendet werden und zeigt, wie die folgende generische Klasse getestet wird

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

Die Tests Ihrer Klasse können jetzt als geschrieben werden. (Vielleicht möchten Sie den Test in "Ereignis ist da" und "Ereignis mit korrektem Namen ausgelöst" aufteilen - Sie können dies selbst tun.)

[TestMethod]
public void EveryWriteablePropertyImplementsINotifyPropertyChangedCorrect()
{
    var viewModel = new TestMyClassWithINotifyPropertyChangedInterface();
    Assert.AreEqual(true, NotificationTester.Verify(viewModel));
}

Klasse

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");
            }
        }
}
WhileTrueSleep
quelle
Ich habe es versucht, aber wenn meine Eigenschaftssetzer eine Schutzanweisung wie "if (value == _myValue) return" haben, was alle meine tun, dann funktioniert das oben genannte nicht, es sei denn, ich vermisse etwas. Ich bin kürzlich von C ++ nach C # gekommen.
Codah
0

Ich habe hier eine Erweiterung vorgenommen:

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

Es gibt die Verwendung:

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

    }
Mr.B.
quelle