Как я могу модульно протестировать то, что использует VisualTreeHelper?

У меня есть эта статическая вспомогательная функция:

    public static DependencyObject GetParentObject(DependencyObject child)
    {
        if (child == null) return null;
        ContentElement contentElement = child as ContentElement;

        if (contentElement != null)
        {
            var parent = ContentOperations.GetParent(contentElement);
            if (parent != null) return parent;

            var fce = contentElement as FrameworkContentElement;
            return fce != null ? fce.Parent : null;
        }

        //if it's not a ContentElement, rely on VisualTreeHelper
        return VisualTreeHelper.GetParent(child);
    }

Он работает в реальном приложении, но я пытаюсь написать для него несколько модульных тестов. Вот моя первая попытка:

    [Test]
    public void GetParentObject_returns_immediate_parent()
    {
        var contentControl = new ContentControl();
        var textBox = new TextBox();

        contentControl.BeginInit();
        contentControl.Content = textBox;
        contentControl.EndInit();

        var result = UIHelper.GetParentObject(textBox);
        Assert.AreSame(contentControl, result);
    }

К сожалению, это не удается, потому что VisualTreeHelper возвращается ноль. Как я могу создать визуальное дерево, которое будет работать?

3 ответа

Решение

Вот почему статика проблематична.

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

public class Foo
{
    static IVisualTreeHelper visualTreeHelper;

    static Foo()
    {
        Foo.visualTreeHelper = new FrameworkVisualTreeHelper();
    }

    public Foo(IVisualTreeHelper visualTreeHelper)
    {
        Foo.visualTreeHelper = visualTreeHelper;
    }

    public static DependencyObject GetParentObject(DependencyObject child)
   {
       if (child == null) return null;
       ContentElement contentElement = child as ContentElement;

       if (contentElement != null)
       {
           var parent = ContentOperations.GetParent(contentElement);
           if (parent != null) return parent;

           var fce = contentElement as FrameworkContentElement;
           return fce != null ? fce.Parent : null;
       }

       //if it's not a ContentElement, rely on the IVisualTreeHelper
       return visualTreeHelper.GetParent(child);
   }
}

public interface IVisualTreeHelper
{
    DependencyObject GetParent(DependencyObject reference);
}

public class FrameworkVisualTreeHelper : IVisualTreeHelper
{
    public DependencyObject GetParent(DependencyObject reference)
    {
        return VisualTreeHelper.GetParent(reference);
    }
}

Очевидно, вам может понадобиться добавить другие VisualTreeHelper методы для вашего интерфейса и реализации по умолчанию, если вы используете другие методы в другом месте.

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

Основываясь на этом ответе здесь о печати документов через Wpf-элементы управления и преобразовании в XPS, я предложил следующий метод расширения для создания визуального дерева. Он хорошо работает в NUnit без STA-потока или чего-либо еще.

/// <summary>
/// Render a UIElement such that the visual tree is generated, 
/// without actually displaying the UIElement
/// anywhere
/// </summary>
public static void CreateVisualTree(this UIElement element)
{
    var fixedDoc = new FixedDocument();
    var pageContent = new PageContent();
    var fixedPage = new FixedPage();
    fixedPage.Children.Add(element);
    pageContent.ToMaybeOf<IAddChild>().Do(c => c.AddChild(fixedPage));
    fixedDoc.Pages.Add(pageContent);

    var f = new XpsSerializerFactory();
    var w = f.CreateSerializerWriter(new MemoryStream());
    w.Write(fixedDoc);
}

Обратите внимание, что

  • другой ответ использует API-интерфейс Reach-dll, который не похож на API, который я вижу. Я предполагаю, что есть различия между.NEt Framework версий 3.5 и 4.0
  • ToMaybeOf материал в основном означает лечить pageContent как IAddChild и сделать действие на этом интерфейсе
  • это не будет работать с элементом типа Window, так как этот элемент по существу добавляется как дочерний элемент к Visual, и Window будет горько жаловаться на это.

Чтобы смоделировать визуальное дерево, вам придется его создать и визуализировать. Таким образом, вам придется создать реальное окно, которое не идеально подходит для юнит-теста.

Другие вопросы по тегам