Prueba unitaria de que los eventos se generan en C # (en orden)

160

Tengo un código que genera PropertyChangedeventos y me gustaría poder hacer una prueba unitaria de que los eventos se están generando correctamente.

El código que está generando los eventos es como

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

Recibo una buena prueba verde del siguiente código en mis pruebas unitarias, que usa delegados:

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

Sin embargo, si luego trato de encadenar la configuración de propiedades de la siguiente manera:

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

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

Mi prueba para el evento falla: el evento que captura es el evento para MyOtherProperty.

Estoy bastante seguro de que el evento se dispara, mi interfaz de usuario reacciona como lo hace, pero mi delegado solo captura el último evento para disparar.

Entonces me pregunto:
1. ¿Es correcto mi método de prueba de eventos?
2. ¿Es correcto mi método de generar eventos encadenados ?

David Hall
fuente

Respuestas:

190

Todo lo que ha hecho es correcto, siempre que desee que su prueba pregunte "¿Cuál es el último evento que se planteó?"

Su código está activando estos dos eventos, en este orden

  • Propiedad modificada (... "Mi propiedad" ...)
  • Propiedad modificada (... "MyOtherProperty" ...)

Si esto es "correcto" o no depende del propósito de estos eventos.

Si desea probar la cantidad de eventos que se generan y el orden en que se generan, puede ampliar fácilmente su prueba existente:

[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
fuente
13
Versión más corta: myClass.PropertyChanged + = (remitente del objeto, e) => recibidoEvents.Add (e.PropertyName);
ShloEmi
22

Si está haciendo TDD, las pruebas de eventos pueden comenzar a generar mucho código repetitivo. Escribí un monitor de eventos que permite un enfoque mucho más limpio para la escritura de pruebas unitarias para estas situaciones.

var publisher = new PropertyChangedEventPublisher();

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

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

EventMonitor.Assert(test, publisher, expectedSequence);

Por favor vea mi respuesta a lo siguiente para más detalles.

Prueba de unidad de que un evento se genera en C #, utilizando la reflexión

Tim Lloyd
fuente
3
El segundo enlace está caído.
Lennart
10

Esto es muy antiguo y probablemente ni siquiera se leerá, pero con algunas características nuevas de .net, he creado una clase INPC Tracer que permite que:

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

Ver resumen: https://gist.github.com/Seikilos/6224204

Samuel
fuente
Hermoso: deberías considerar empaquetarlo y publicarlo en nuget.org
Simon Ejsing
1
¡Buen trabajo! Realmente cavo la API fluida. Yo mismo he hecho algo similar ( github.com/f-tischler/EventTesting ) pero creo que su enfoque es aún más conciso.
Florian Tischler
6

A continuación se muestra un código de Andrew ligeramente modificado que, en lugar de simplemente registrar la secuencia de eventos generados, cuenta cuántas veces se ha llamado a un evento específico. Aunque está basado en su código, lo encuentro más útil en mis pruebas.

[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
fuente
1

Basado en este artículo, he creado este simple asistente de afirmación:

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

Con este método auxiliar, la prueba se vuelve realmente simple.

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

No escriba una prueba para cada miembro: esto es mucho trabajo

(tal vez esta solución no sea perfecta para cada situación, pero muestra una forma posible. Es posible que deba adaptarla para su caso de uso)

Es posible usar la reflexión en una biblioteca para probar si todos sus miembros están respondiendo correctamente a su evento de cambio de propiedad:

  • El evento PropertyChanged se genera en el acceso del configurador
  • El evento se genera correctamente (el nombre de la propiedad es igual al argumento del evento generado)

El siguiente código se puede usar como una biblioteca y muestra cómo probar la siguiente clase genérica

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

Las pruebas de tu clase ahora se pueden escribir como. (tal vez desee dividir la prueba en "evento está allí" y "evento generado con el nombre correcto"; puede hacerlo usted mismo)

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

Clase

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");
            }
        }
}
Mientras VerdaderoSueño
fuente
Intenté esto, pero si mis establecedores de propiedades tienen una declaración de protección como "if (value == _myValue) return", que todos los míos hacen, entonces lo anterior no funcionará, a menos que me falte algo. Recientemente he venido de C ++ a C #.
codah
0

He hecho una extensión aquí:

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

Existe el uso:

   [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
fuente