单元测试自定义WPF UIElement的呈现

时间:2014-04-08 11:59:59

标签: c# wpf unit-testing

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

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

2 个答案:

答案 0 :(得分:5)

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

但是我仍然将答案复制到这个帖子中(有人可能会删除它,如果不需要的话)。

解决方案是,从DrawingContext创建DrawingGroup

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

答案 1 :(得分:0)

不幸的是,您必须致电InvalidateVisual,在内部调用InvalidateArrange。 OnRender方法作为排列阶段的一部分被调用,因此您需要告诉WPF重新排列控件(InvalidateArrange会这样做)并且需要重绘(InvalidateVisual会这样做)。

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

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

您可以通过将渲染逻辑移动到单独的控件(比如NestedControl)来解决这个问题,该控件将是MainControl的可视子级。 MainControl可以将其作为可视子项自动添加或作为其ControlMmplate的一部分添加,但它需要是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" />

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

MeasureOverride调用MainTestPanel。 MeasureOverride调用了InnerPanel。 ArrangeOverride调用MainTestPanel。 ArrangeOverride调用了InnerPanel。 OnRender呼吁InnerPanel。 OnRender呼吁MainTestPanel。 但是当我在&#39; MainTestPanel&#39;上调用InvalidateVisual时(在按钮&#39; Click&#39;事件中),我得到了这个......

为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。