Eenheidstest dat gebeurtenissen worden gegenereerd in C# (in volgorde)

Ik heb een code die PropertyChanged-gebeurtenissen genereert en ik zou graag een eenheidstest willen doen om te controleren of de gebeurtenissen correct worden gegenereerd.

De code die de evenementen doet stijgen is als

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

Ik krijg een mooie groene test van de volgende code in mijn unit tests, die gedelegeerden gebruikt:

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

Echter, als ik dan probeer de instellingen van eigenschappen als volgt aan elkaar te koppelen:

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

Mijn test voor de gebeurtenis mislukt – de gebeurtenis die wordt vastgelegd, is de gebeurtenis voor de MyOtherProperty.

Ik ben er vrij zeker van dat de gebeurtenis wordt geactiveerd, mijn gebruikersinterface reageert zoals hij doet, maar mijn gemachtigde legt alleen de laatste gebeurtenis vast die wordt geactiveerd.

Dus ik vraag me af:
1. Is mijn methode om gebeurtenissen te testen correct?
2. Is mijn methode om geketendegebeurtenissen te verhogen correct?


Antwoord 1, autoriteit 100%

Alles wat je hebt gedaan is correct, op voorwaarde dat je wilt dat je test vraagt: “Wat is het laatste evenement dat is gerezen?”

Je code activeert deze twee gebeurtenissen, in deze volgorde

  • Eigendom gewijzigd (… “Mijn eigendom” …)
  • Eigenschap gewijzigd (… “MyOtherProperty” …)

Of dit “juist” is of niet, hangt af van het doel van deze evenementen.

Als je het aantal evenementen wilt testen dat wordt verhoogd en de volgorde waarin ze worden verhoogd, kun je eenvoudig je bestaande test uitbreiden:

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

Antwoord 2, autoriteit 11%

Als je TDD doet, kan het testen van gebeurtenissen beginnen om veelherhalende code te genereren. Ik heb een gebeurtenismonitor geschreven die een veel schonere benadering mogelijk maakt voor het schrijven van eenheidstests voor deze situaties.

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

Zie mijn antwoord op het volgende voor meer details.

Eenheidstest die een gebeurtenis wordt opgewekt in C#, met behulp van reflectie


Antwoord 3, autoriteit 6%

Dit is erg oud en zal waarschijnlijk niet eens worden gelezen, maar met een aantal coole nieuwe .net-functies heb ik een INPC Tracer-klasse gemaakt die dat mogelijk maakt:

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

Zie in wezen: https://gist.github.com/Seikilos/6224204


Antwoord 4, autoriteit 3%

Hieronder staat een licht gewijzigde Andrew’s code die in plaats van alleen de volgorde van verhoogde gebeurtenissen te loggen, telt hoe vaak een specifieke gebeurtenis is aangeroepen. Hoewel het op zijn code is gebaseerd, vind ik het nuttiger in mijn tests.

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

Antwoord 5

Op basis van dit artikel heb ik deze eenvoudige hulp bij beweringen gemaakt:

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

Met deze methodehulp wordt de test heel eenvoudig.

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

Antwoord 6

Schrijf geen test voor elk lid – dit is veel werk

(misschien is deze oplossing niet perfect voor elke situatie – maar het toont een mogelijke manier. Mogelijk moet u het aanpassen aan uw gebruiksgeval)

Het is mogelijk om reflectie in een bibliotheek te gebruiken om te testen of al je leden correct reageren op je evenement dat van eigendom is veranderd:

  • PropertyChanged-gebeurtenis wordt gegenereerd bij toegang tot setter
  • Gebeurtenis is correct gegenereerd (naam van eigenschap is gelijk aan argument van verhoogde gebeurtenis)

De volgende code kan als bibliotheek worden gebruikt en laat zien hoe u de volgende generieke klasse kunt testen

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

De toetsen van uw klas kunnen nu worden geschreven als. (misschien wil je de test opsplitsen in “event is daar” en “event met de juiste naam” – je kunt dit zelf doen)

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

Antwoord 7

Ik heb hier een extensie gemaakt:

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

Er is het gebruik:

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

Other episodes