Test unitaire que les événements sont déclenchés en C # (dans l'ordre)

160

J'ai du code qui déclenche des PropertyChangedévénements et j'aimerais pouvoir tester unitaire que les événements sont déclenchés correctement.

Le code qui déclenche les événements est comme

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

J'obtiens un joli test vert à partir du code suivant dans mes tests unitaires, qui utilise des délégués:

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

Cependant, si j'essaie ensuite de chaîner le réglage des propriétés comme ceci:

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

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

Mon test pour l'événement échoue - l'événement qu'il capture est l'événement pour MyOtherProperty.

Je suis presque sûr que l'événement se déclenche, mon interface utilisateur réagit comme il le fait, mais mon délégué ne capture que le dernier événement à déclencher.

Je me demande donc:
1. Ma méthode de test des événements est-elle correcte?
2. Ma méthode de génération d' événements chaînés est-elle correcte?

David Hall
la source

Réponses:

190

Tout ce que vous avez fait est correct, à condition que vous souhaitiez que votre test vous demande "Quel est le dernier événement qui a été soulevé?"

Votre code déclenche ces deux événements, dans cet ordre

  • Propriété modifiée (... "Ma propriété" ...)
  • Propriété modifiée (... "MyOtherProperty" ...)

Que ce soit "correct" ou non dépend du but de ces événements.

Si vous souhaitez tester le nombre d'événements déclenchés et l'ordre dans lequel ils sont déclenchés, vous pouvez facilement étendre votre test existant:

[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
la source
13
Version plus courte: myClass.PropertyChanged + = (expéditeur de l'objet, e) => receiveEvents.Add (e.PropertyName);
ShloEmi
22

Si vous faites du TDD, les tests d'événement peuvent commencer à générer beaucoup de code répétitif. J'ai écrit un moniteur d'événements qui permet une approche beaucoup plus propre de l'écriture de tests unitaires pour ces situations.

var publisher = new PropertyChangedEventPublisher();

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

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

EventMonitor.Assert(test, publisher, expectedSequence);

Veuillez consulter ma réponse à ce qui suit pour plus de détails.

Test unitaire qu'un événement est déclenché en C #, à l'aide de la réflexion

Tim Lloyd
la source
3
Le deuxième lien est en panne.
Lennart
10

Ceci est très ancien et ne sera probablement même pas lu, mais avec de nouvelles fonctionnalités .net cool, j'ai créé une classe INPC Tracer qui permet cela:

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

Voir l'essentiel: https://gist.github.com/Seikilos/6224204

Samuel
la source
Magnifique - vous devriez envisager de l'empaqueter et de le publier sur nuget.org
Simon Ejsing
1
Bon travail! Je creuse vraiment l'API fluide. J'ai fait quelque chose de similaire moi-même ( github.com/f-tischler/EventTesting ) mais je pense que votre approche est encore plus concise.
Florian Tischler
6

Vous trouverez ci-dessous un code d'Andrew légèrement modifié qui, au lieu de simplement consigner la séquence d'événements déclenchés, compte plutôt le nombre de fois qu'un événement spécifique a été appelé. Bien qu'il soit basé sur son code, je le trouve plus utile dans mes 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"]);
}
Damir Arh
la source
1

Sur la base de cet article, j'ai créé cet assistant d'assertion simple:

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

Avec cette aide de méthode, le test devient vraiment simple.

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

N'écrivez pas de test pour chaque membre - c'est beaucoup de travail

(peut-être que cette solution n'est pas parfaite pour toutes les situations - mais elle montre une solution possible. Vous devrez peut-être l'adapter à votre cas d'utilisation)

Il est possible d'utiliser la réflexion dans une bibliothèque pour tester si vos membres répondent tous correctement à l'événement de modification de votre propriété:

  • L'événement PropertyChanged est déclenché lors de l'accès du setter
  • L'événement est déclenché correctement (le nom de la propriété est égal à l'argument de l'événement déclenché)

Le code suivant peut être utilisé comme bibliothèque et montre comment tester la classe générique suivante

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

Les tests de votre classe peuvent maintenant être écrits comme. (peut-être voulez-vous diviser le test en "événement est là" et "événement déclenché avec le nom correct" - vous pouvez le faire vous-même)

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

Classe

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
la source
J'ai essayé cela, mais si mes setters de propriété ont une déclaration de garde comme "if (value == _myValue) return", ce que font tous les miens, alors ce qui précède ne fonctionnera pas, sauf si je manque quelque chose. Je suis récemment passé du C ++ au C #.
codah
0

J'ai fait une extension ici:

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

Il y a l'utilisation:

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

    }
Monsieur B
la source