【问题标题】:Unit testing the rendering of a custom WPF UIElement单元测试自定义 WPF UIElement 的呈现
【发布时间】:2014-04-08 11:59:58
【问题描述】:

在我的应用程序中,我使用了一个自定义的UIElement,它处理自己的布局和渲染。虽然我可以对其中的大部分内容进行单元测试,但我无法正确地对渲染进行单元测试。原因是渲染是通过OnRender 方法完成的,但我无法测试实际渲染的内容,因为 DrawingContext 是一个带有内部构造函数的抽象类,所以我不能从它派生来进行测试。

我知道如何做的唯一测试是尝试不同的场景(基于实现的代码)并检查是否没有抛出异常。有什么方法可以测试更多(除了使用 TypeMock Isolator 或 JustMock)?

【问题讨论】:

  • 您是否尝试在控件呈现后对其进行测试?
  • @lll 我正在尝试测试OnRender() 方法本身。在那里,我绘制背景颜色、绘制线条、绘制圆圈等。我可以测试的所有其他内容(例如测量、命中测试)。

标签: c# wpf unit-testing


【解决方案1】:

我在这个帖子里写了答案:unit testing custom OnRender-Method

不过,我也将答案复制到了这个线程(如果不需要,有人可能会删除它)。

解决方案是从DrawingGroup 创建DrawingContext

public class TestingMyControl : MyControl
{
    public DrawingGroup Render()
    {
        var drawingGroup = new DrawingGroup();
        using (var drawingContext = drawingGroup.Open())
        {
             base.OnRender(drawingContext);
        }
        return drawingGroup;
    }
}

所以夹具看起来像:

    [Test]
    public void Should_render()
    {
        var controlToTest = new TestingMyControl();

        var drawingGroup = controlToTest.Render();

        var drawing = drawingGroup.Children[0] as GeometryDrawing;
        Assert.That(drawing.Brush, Is.EqualTo(Brushes.Black));
        Assert.That(drawing.Pen.Brush, Is.EqualTo(Brushes.SeaGreen));
        Assert.That(drawing.Pen.Thickness, Is.EqualTo(0.6));
        Assert.That(drawing.Bounds.X, Is.EqualTo(5));
        Assert.That(drawing.Bounds.Y, Is.EqualTo(15));
        Assert.That(drawing.Bounds.Width, Is.EqualTo(25));
        Assert.That(drawing.Bounds.Height, Is.EqualTo(35));
    }

这需要以下生产代码:

public class MyControl : Canvas
{
    protected override void OnRender(DrawingContext dc)
    {
        dc.DrawRectangle(Brushes.Black, new Pen(Brushes.SeaGreen, 0.6), new Rect(5, 15, 25, 35));
    }
}

【讨论】:

    【解决方案2】:

    很遗憾,您必须调用InvalidateVisual,它在内部调用InvalidateArrange。 OnRender 方法作为排列阶段的一部分被调用,因此您需要告诉 WPF 重新排列控件(InvalidateArrange 执行此操作)并且它需要重绘(InvalidateVisual 执行此操作)。

    FrameworkPropertyMetadata.AffectsRender 选项只是告诉 WPF 在关联属性更改时调用 InvalidateVisual

    如果您有一个覆盖OnRender 并包含多个后代控件的控件(我们称之为MainControl),那么调用InvalidateVisual 可能需要重新排列甚至重新测量后代控件。但我相信 WPF 已经进行了优化,以防止在其可用空间不变的情况下重新排列后代控件。

    您可以通过将渲染逻辑移至单独的控件(例如 NestedControl)来解决此问题,该控件将是 MainControl 的可视子控件。 MainControl 可以自动将其添加为可视子项或作为其 ControlTemplate 的一部分,但它必须是 z 顺序中最低的子项。然后,您可以在 MainControl 上公开一个 InvalidateNestedControl 类型的方法,该方法将在 NestedControl 上调用 InvalidateVisual。

    这就是我所做的。为了测试这一点,我创建了这个子类...

    public class TestPanel : DockPanel
    {
        protected override Size MeasureOverride(Size constraint)
        {
            System.Console.WriteLine("MeasureOverride called for " + this.Name + ".");
            return base.MeasureOverride(constraint);
        }
    protected override System.Windows.Size ArrangeOverride(System.Windows.Size arrangeSize)
    {
        System.Console.WriteLine("ArrangeOverride called for " + this.Name + ".");
        return base.ArrangeOverride(arrangeSize);
    }
    
    protected override void OnRender(System.Windows.Media.DrawingContext dc)
    {
        System.Console.WriteLine("OnRender called for " + this.Name + ".");
        base.OnRender(dc);
    }
    
    }
    

    ...我这样布置(注意它们是嵌套的):

    <Button Content="Test" Click="Button_Click" DockPanel.Dock="Top" HorizontalAlignment="Left" />
    
    <l:TestPanel x:Name="InnerPanel" Background="Red" Margin="16" />
    

    当我调整窗口大小时,我得到了这个......

    为 MainTestPanel 调用了MeasureOverride。 MeasureOverride 调用了 InnerPanel。 为 MainTestPanel 调用了 ArrangeOverride。 为 InnerPanel 调用了 ArrangeOverride。 OnRender 调用了 InnerPanel。 OnRender 调用 MainTestPanel。 但是当我在“MainTestPanel”上(在按钮的“Click”事件中)调用 InvalidateVisual 时,我得到了这个......

    为 MainTestPanel 调用了 ArrangeOverride。 OnRender 调用 MainTestPanel。 注意没有调用任何测量覆盖,只调用了外部控件的 ArrangeOverride。

    这并不完美,好像您在子类的 ArrangeOverride 中进行了非常繁重的计算(不幸的是,我们这样做了)仍然会被(重新)执行,但至少孩子们不会落入同样的命运。

    但是,如果您知道没有一个子控件具有设置了 AffectsParentArrange 位的属性(同样,我们这样做了),您可以更好地使用 Nullable Size 作为标志来抑制 ArrangeOverride 逻辑重新除非需要,否则进入,就像这样......

    public class TestPanel : DockPanel
    {
        Size? arrangeResult;
    
    protected override Size MeasureOverride(Size constraint)
    {
        arrangeResult = null;
        System.Console.WriteLine("MeasureOverride called for " + this.Name + ".");
        return base.MeasureOverride(constraint);
    }
    
    protected override System.Windows.Size ArrangeOverride(System.Windows.Size arrangeSize)
    {
        if(!arrangeResult.HasValue)
        {
            System.Console.WriteLine("ArrangeOverride called for " + this.Name + ".");
            // Do your arrange work here
            arrangeResult = base.ArrangeOverride(arrangeSize);
        }
    
        return arrangeResult.Value;
    }
    
    protected override void OnRender(System.Windows.Media.DrawingContext dc)
    {
        System.Console.WriteLine("OnRender called for " + this.Name + ".");
        base.OnRender(dc);
    }
    
    }
    

    现在,除非有什么特别需要重新执行排列逻辑(就像调用 MeasureOverride 那样),否则您只会获得 OnRender,如果您想显式强制排列逻辑,只需将大小清零,调用 InvalidateVisual。

    【讨论】:

    • 不幸的是,这个详细的答案并没有真正帮助我。拨打OnRender() 没问题。我想对它进行单元测试(使用 NUnit)。我的 UIElement 中的代码绘制了背景颜色,然后绘制了一些圆圈和线条。在测试中,我发现没有办法检查这是否真的完成了,所以我只能检查没有异常发生..
    • @DanielRose 您使用的是 .NET 4.5 吗?您可以尝试使用假程序集吗?
    • 我在 VS 2013 Professional 上使用 .NET 4.0。使用伪造的程序集(例如 TypeMock)要花很多钱,我认为没有理由仅仅为了测试而购买这样的工具。
    • @DanielRose 否如果您使用的是 .NET 4.5,则可以免费使用 Fake assemblies
    • Microsoft Fakes 需要 VS Premium 或 Ultimate。这就是为什么我写我正在使用 VS Professional。升级到 Premium/Ultimate 不是免费的。
    猜你喜欢
    • 1970-01-01
    • 2011-11-05
    • 1970-01-01
    • 2018-02-25
    • 2015-01-11
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2023-03-28
    相关资源
    最近更新 更多