В MVVM с WPF, как мне протестировать связь между ViewModel и View - PullRequest
20 голосов
/ 18 февраля 2010

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

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

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

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

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

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


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

Ответы [ 5 ]

5 голосов
/ 06 октября 2010

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

[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);
            }
        }
    }
}
3 голосов
/ 18 февраля 2010

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

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

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 - Обнаружение ошибок привязки

Надеюсь, это поможет. Хотя бы немного:).

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

1 голос
/ 06 марта 2013

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

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

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

1 голос
/ 05 октября 2010

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

Если вам известно имя элемента управления, с которым вы ожидаете связать, вы можете выполнить что-то вроде теста ниже (используя 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>
0 голосов
/ 18 февраля 2010

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

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

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

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...