Подтвердить что ты не робот

В MVVM с WPF как unit test ссылка между ViewModel и представлением

В MVVM нормально подключаться к ViewModel с привязкой данных.

Поэтому, если имя свойства изменяется на одном из объектов Model, которое привязано к базе данных, там нет ошибки компилятора.

Когда компилятор не остановит ошибку, следующая вещь, о которой я думаю, это "UnitTest", однако

Как вы unit test это без тратить навсегда письменное тестирование GUI?

Есть ли система, которая проверит, что все связанные с этим свойства действительны (без необходимости запуска UI), которые я могу вызвать в unit test?

Я ищу что-то, что займет представление, а затем перебирает все элементы управления WPF, для каждого элемента управления WPF он будет смотреть на всю привязку и проверять, действительны ли они.


Кстати, было несколько хороших вопросов о том, как сделать OnPropertyChanged безопасным и/или как его протестировать (но сделанные из них доходят до уровня представления WPF.)


Я поставил щедрость на этот вопрос, так как кто-то должен был подумать о проблеме и придумать soltions.

4b9b3361

Ответ 1

Я думаю, что я придумал что-то, что может работать с использованием простого отражения, и адаптируя некоторый код, который я использовал в прошлом (код для метода FindBindingsRecursively был написан Мартином Беннидиком как часть его Enterprise Validation Control):

[TestMethod]
public void CheckWpfBindingsAreValid()
{
    // instansiate the xaml view and set DataContext
    var yourView = new YourView(); 
    yourView.DataContext = YourViewModel;

    FindBindingsRecursively(yourView,
            delegate(FrameworkElement element, Binding binding, DependencyProperty dp)
            {
                var type = yourView.DataContext.GetType();

                // check that each part of binding valid via reflection
                foreach (string prop in binding.Path.Path.Split('.'))
                {
                    PropertyInfo info = type.GetProperty(prop);
                    Assert.IsNotNull(info);
                    type = info.PropertyType;
                }
    });
}

private delegate void FoundBindingCallbackDelegate(FrameworkElement element, Binding binding, DependencyProperty dp);

private void FindBindingsRecursively(DependencyObject element, FoundBindingCallbackDelegate callbackDelegate)
{
    // See if we should display the errors on this element
    MemberInfo[] members = element.GetType().GetMembers(BindingFlags.Static |
                BindingFlags.Public |
                BindingFlags.FlattenHierarchy);

    foreach (MemberInfo member in members)
    {
        DependencyProperty dp = null;

        // Check to see if the field or property we were given is a dependency property
        if (member.MemberType == MemberTypes.Field)
        {
            FieldInfo field = (FieldInfo)member;
            if (typeof(DependencyProperty).IsAssignableFrom(field.FieldType))
            {
                dp = (DependencyProperty)field.GetValue(element);
            }
        }
        else if (member.MemberType == MemberTypes.Property)
        {
            PropertyInfo prop = (PropertyInfo)member;
            if (typeof(DependencyProperty).IsAssignableFrom(prop.PropertyType))
            {
                dp = (DependencyProperty)prop.GetValue(element, null);
            }
        }

        if (dp != null)
        {
            // Awesome, we have a dependency property. does it have a binding? If yes, is it bound to the property we're interested in?
            Binding bb = BindingOperations.GetBinding(element, dp);
            if (bb != null)
            {
                // This element has a DependencyProperty that we know of that is bound to the property we're interested in. 
                // Now we just tell the callback and the caller will handle it.
                if (element is FrameworkElement)
                {
                    callbackDelegate((FrameworkElement)element, bb, dp);
                }
            }
        }
    }

    // Now, recurse through any child elements
    if (element is FrameworkElement || element is FrameworkContentElement)
    {
        foreach (object childElement in LogicalTreeHelper.GetChildren(element))
        {
            if (childElement is DependencyObject)
            {
                FindBindingsRecursively((DependencyObject)childElement, callbackDelegate);
            }
        }
    }
}

Ответ 2

Действительно хороший вопрос. Проголосовал. Я тоже хотел бы знать ответ.

Один из лучших практик, которые я знаю (предложенный Джошем Смитом, спасибо, что Gishu указал на это), имеет класс модели базового представления проверьте метод OnPropertyChanged(), существует ли свойство. Например:.

abstract class ViewModelBase
{
    [Conditional("DEBUG")]
    public void VerifyPropertyName(string propertyName)
    {
        // Verify that the property name matches a real,  
        // public, instance property on this object.
        if (TypeDescriptor.GetProperties(this)[propertyName] == null)
        {
            if (this.ThrowOnInvalidPropertyName)
                throw new ArgumentException(propertyName);

            string msg = "Invalid property name: " + propertyName;
            Debug.Fail(msg);
        }
    }

    protected void OnPropertyChanged(string propertyName)
    {
        VerifyPropertyName(propertyName);

        PropertyChangedEventHandler handler = this.PropertyChanged;
        if (handler != null)
        {
            var e = new PropertyChangedEventArgs(propertyName);
            handler(this, e);
        }
    }
}

Но это не помогло бы вам найти проблемы с орфографией в XAML. Хм... Я не знаю ни одного существующего решения для этого. Может быть, ребята из WPF Disciples могут что-то предложить. Я думаю, что я бы пошел с PresentationTraceSources.DataBindingSource и добавить в его экземпляр коллекции Listners TextWriterTraceListener, а затем отслеживать вывод. Как только мы получим сообщение об ошибке или предупреждение на нашем радаре, мы должны пройти тест.

Нашел хороший пример: Фрагмент WPF - Обнаружение ошибок привязки

Надеюсь, это поможет. По крайней мере, бит:).

Приветствия, Анвака.

Ответ 3

Я знаю, что это не прямой ответ на ваш вопрос.

Если вы знаете имя элемента управления, которое вы ожидаете связать с собой, вы можете сделать что-то вроде теста ниже (используя nunit). Это грубая версия. Но здесь вы используете выражения и явно проверяете, что свойство находится в привязке

 [Test]
    public void TestBindings()
    {
        TestBinding<IndividualSolutionViewModel, string>(x => x.Name, "Name", TextBlock.TextProperty);
    }

    private void TestBinding<TViewModel,TResult>(Expression<Func<TViewModel, TResult>> property, string elementName, 
        DependencyProperty dependencyProperty)
    {
        string memberName = ExpressionHelper.GetPropertyName(property); // f.ex v => v.Name will return Name
        TestBinding(memberName, elementName, dependencyProperty);

    }

    private void TestBinding(string memberName, string elementInControlName, DependencyProperty dependencyProperty)
    {
        //object viewModel = new IndividualSolutionViewModel();
        var view = new IndividualSolutionView();

        //Assert.That(view.DataContext, Is.EqualTo(viewModel));

        var element = view.FindName(elementInControlName);
        Assert.That(element, Is.Not.Null, string.Format("Unable to find the element {0} in view {1}", elementInControlName, view.Name));
        Assert.That(element, Is.InstanceOf(typeof(DependencyObject)));

        var binding = BindingOperations.GetBinding(element as DependencyObject, dependencyProperty);
        Assert.That(binding, Is.Not.Null, string.Format("Could not find a binding for the control {0}", elementInControlName));

        Assert.That(binding.Path.Path, Is.EqualTo(memberName));
    }

Ps. Вы должны добавить это в app.config

<configSections>
    <sectionGroup name="NUnit">
        <section type="System.Configuration.NameValueSectionHandler"
                 name="TestRunner"></section>
    </sectionGroup>
</configSections>
<NUnit>
    <TestRunner>
        <add value="STA" key="ApartmentState"></add>
    </TestRunner>
</NUnit>

Ответ 4

Существует также такая возможность, которая может дать вам некоторые идеи. Суть идеи заключается в том, что имена свойств, к которым вы привязывались, отображаются как статические свойства строки. Если имя свойства привязки изменилось, вы получите ошибку компиляции.

У меня не было возможности действительно испытать эту технику самостоятельно, но это выглядит интересно:

http://www.codeproject.com/Articles/42036/Project-Metadata-Generation-using-T4

Ответ 5

Как указывает Анвака, использование базового класса для вашей модели представления, которая проверяет имена свойств, может помочь избежать этой конкретной проблемы (хотя она не скажет вам, когда ваш класс VM выполняет собственное уведомление об изменении свойства и игнорирует метод в базовый класс, а не то, что я когда-либо видел, что подобное происходит в моем коде).

И вы можете (и должны) использовать ваш код, чтобы все, что не работает, не срабатывало так, как вам кажется. То, что парадоксально в том, что, если вы знаете, что может потерпеть неудачу, и вы их смотрите, они не будут, потому что тот факт, что вы смотрите за ними, не позволит вам совершать ошибки, которые приводят их к неудаче (например, написав селектор шаблонов, который не всегда возвращает шаблон).

Но в принципе, представление является пользовательским интерфейсом, поэтому я был бы очень удивлен, найдя методы его тестирования, которые не были также методами тестирования пользовательского интерфейса.