Testowanie jednostkowe, które zdarzenia są wywoływane w C # (w kolejności)


160

Mam kod, który wywołuje PropertyChangedzdarzenia i chciałbym móc przetestować jednostkowo, czy zdarzenia są wywoływane poprawnie.

Kod, który wywołuje zdarzenia, jest podobny

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

Otrzymuję ładny zielony test z następującego kodu w moich testach jednostkowych, który używa delegatów:

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

Jeśli jednak spróbuję połączyć ustawienia właściwości w następujący sposób:

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

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

Mój test na zdarzenie kończy się niepowodzeniem - zdarzenie, które przechwytuje, jest zdarzeniem dla MyOtherProperty.

Jestem prawie pewien, że zdarzenie się uruchamia, mój interfejs użytkownika reaguje tak, jak robi, ale mój delegat rejestruje tylko ostatnie zdarzenie do odpalenia.

Zastanawiam się więc:
1. Czy moja metoda testowania zdarzeń jest poprawna?
2. Czy moja metoda zgłaszania zdarzeń łańcuchowych jest poprawna?

Odpowiedzi:


190

Wszystko, co zrobiłeś, jest poprawne, pod warunkiem, że chcesz, aby test zawierał pytanie „Jakie było ostatnie zgłoszone zdarzenie?”

Twój kod uruchamia te dwa zdarzenia w tej kolejności

  • Właściwość zmieniona (... "Moja nieruchomość" ...)
  • Właściwość zmieniona (... "MyOtherProperty" ...)

To, czy jest to „poprawne”, czy nie, zależy od celu tych wydarzeń.

Jeśli chcesz sprawdzić liczbę zgłaszanych zdarzeń i kolejność ich zgłaszania, możesz łatwo rozszerzyć istniejący test:

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

13
Krótsza wersja: myClass.PropertyChanged + = (nadawca obiektu, e) => receivedEvents.Add (e.PropertyName);
ShloEmi

22

Jeśli robisz TDD, testowanie zdarzeń może zacząć generować dużo powtarzalnego kodu. Napisałem monitor zdarzeń, który umożliwia znacznie czystsze podejście do pisania testów jednostkowych w takich sytuacjach.

var publisher = new PropertyChangedEventPublisher();

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

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

EventMonitor.Assert(test, publisher, expectedSequence);

Aby uzyskać więcej informacji, zobacz moją odpowiedź na poniższe pytania.

Testowanie jednostkowe, że zdarzenie jest zgłaszane w C # przy użyciu odbicia


3
Drugie łącze nie działa.
Lennart

10

To jest bardzo stare i prawdopodobnie nie będzie nawet czytane, ale dzięki kilku fajnym nowym funkcjom .net utworzyłem klasę INPC Tracer, która pozwala na:

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

Zobacz streszczenie: https://gist.github.com/Seikilos/6224204


Piękne - powinieneś rozważyć zapakowanie go i opublikowanie na nuget.org
Simon

1
Świetna robota! Naprawdę lubię płynne API. Sam zrobiłem coś podobnego ( github.com/f-tischler/EventTesting ), ale myślę, że twoje podejście jest jeszcze bardziej zwięzłe.
Florian Tischler

6

Poniżej znajduje się nieco zmieniony kod Andrew, który zamiast rejestrować sekwencję zgłoszonych zdarzeń, liczy raczej, ile razy zostało wywołane określone zdarzenie. Chociaż jest oparty na jego kodzie, uważam, że jest bardziej przydatny w moich testach.

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

1

Na podstawie tego artykułu stworzyłem prostego pomocnika asercji:

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

Dzięki temu pomocnikowi metody test staje się naprawdę prosty.

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

1

Nie pisz testu dla każdego członka - to dużo pracy

(być może to rozwiązanie nie jest idealne w każdej sytuacji - ale pokazuje możliwy sposób. Może być konieczne dostosowanie go do swojego przypadku użycia)

Możesz użyć odbicia w bibliotece, aby sprawdzić, czy wszyscy członkowie poprawnie reagują na zdarzenie zmiany właściwości:

  • Zdarzenie PropertyChanged jest zgłaszane podczas dostępu do metody ustawiającej
  • Zdarzenie zostało zgłoszone poprawnie (nazwa właściwości równa się argumentowi podniesionego zdarzenia)

Poniższy kod może służyć jako biblioteka i pokazuje, jak przetestować następującą klasę ogólną

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

Testy Twojej klasy można teraz zapisać jako. (może chcesz podzielić test na „zdarzenie istnieje” i „zdarzenie wywołane z poprawną nazwą” - możesz to zrobić samodzielnie)

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

Klasa

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

Próbowałem tego, ale jeśli moje metody ustawiające właściwości mają instrukcję strażnika, taką jak „if (value == _myValue) return”, co robi wszystko moje, powyższe nie zadziała, chyba że czegoś brakuje. Niedawno przeszedłem z C ++ do C #.
codah

0

Zrobiłem tutaj rozszerzenie:

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

Jest zastosowanie:

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

    }
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.