Automatisierte Tests mit Windows.Forms UI

Vor kurzem wurde ich auf einen kleinen aber blöden Fehler in Seeing# hingewiesen. Die Rendering-Hauptschleife ist abgebrochen, sobald man ein Windows.Forms Control von einem Parent ab und an einen anderen angehängt hat. Jetzt klingt das nach einem sehr seltenen Fall, aber auch diesen hätte ich ein paar Jahre zuvor explizit behandelt – allerdings im Rahmen eines Windows.Forms Programms. Im Laufe der Zeit ist dieser Fehler wieder in den Code gewandert, ohne dass er wirklich auffällt. Wer testet sowas auch immer wieder? Aus diesem Grund habe ich versucht, diesen Fall per Testautomatisierung zu behandeln, um so automatisch genau diesen Fall immer wieder abprüfen zu lassen.

Jetzt gibt es natürlich das klassische Problem, dass man UI’s nicht ganz so einfach automatisiert testen kann. Grund dafür sind die Abhänigkeiten, man benötigt z. B. einen UI-Thread, der typischerweise per Application.Run(…) gestartet wird. Zusätzlich benötigt man meistens auch einen Bediener… in meinem Fall zum Glück aber nicht. Ich möchte ja nur das Verhalten testen, wenn das Control, in das Seeing# rendert, von einem Parent zum anderen umgehängt wird.

Schwierig an dem Thema ist auch, dass die Testmethode keine Auswirkungen auf andere Methoden haben soll, welche vorher oder danach ausgeführt werden. Falls dem so wäre, würde man die Aussagekraft dieser Unittests kaputt machen. Das größte Hindernis für mich ist also das Thema mit dem UI-Thread. Verwandle ich einen Thread per Application.Run in einen UI-Thread, so ist dieses Thema durch. Nachfolgend meine erste Lösung für genau dieses Problem [1].

[Fact]
[Trait("Category", TEST_CATEGORY)]
public async Task WinForms_Parent_Child_Switch()
{
    await UnitTestHelper.InitializeWithGrahicsAsync();

    Panel hostPanel1 = null;
    Panel hostPanel2 = null;
    SeeingSharpRendererControl renderControl = null;
    int stepID = 0;
    Exception fakeUIThreadException = null;

    ObjectThread fakeUIThread = new ObjectThread("Fake-UI", 100);
    fakeUIThread.ThreadException += (sender, eArgs) =>
    {
        fakeUIThreadException = eArgs.Exception;
    };
    fakeUIThread.Starting += (sender, eArgs) =>
    {
        hostPanel1 = new System.Windows.Forms.Panel();
        hostPanel1.Size = new Size(500, 500);
        hostPanel2 = new System.Windows.Forms.Panel();
        hostPanel2.Size = new Size(500, 500);

        renderControl = new SeeingSharpRendererControl();
        renderControl.Dock = System.Windows.Forms.DockStyle.Fill;

        hostPanel1.CreateControl();
        hostPanel2.CreateControl();
        hostPanel1.Controls.Add(renderControl);
    };
    fakeUIThread.Tick += (sender, eArgs) =>
    {
        Application.DoEvents();
        stepID++;
        
        switch(stepID)
        {
            case 2:
                hostPanel1.Controls.Remove(renderControl);
                break;

            case 4:
                hostPanel2.Controls.Add(renderControl);
                break;

            case 8:
                hostPanel2.Controls.Remove(renderControl);
                break;

            case 10:
                renderControl.Dispose();
                hostPanel2.Dispose();
                hostPanel1.Dispose();
                break;

            case 11:
                fakeUIThread.Stop();
                break;
        }
    };
    fakeUIThread.Start();

    // Wait until the Fake-UI thread stopped
    await fakeUIThread.WaitUntilSoppedAsync();

    // Some checks after rendering
    Assert.True(GraphicsCore.Current.MainLoop.IsRunning);
    Assert.True(GraphicsCore.Current.MainLoop.RegisteredRenderLoopCount == 0);
    Assert.Null(fakeUIThreadException);
    Assert.True(renderControl.IsDisposed);
}

Wie in dem Coding zu sehen umgehe ich das Problem mit dem UI-Thread dadurch, dass ich einen ObjectThread anlege. ObjectThread ist dabei eine Klasse aus Seeing#, welche im Hintergrund einen Thread mit einer Hauptschleife startet, welcher wiederum Starting, Tick und Stopping Ereignisse feuert. Das Ereignis Tick wird dabei zyklisch gefeuert, solange der Thread läuft. Der Trick ist jetzt, dass ich innerhalb von Tick Application.DoEvents aufrufe und somit diesen Thread zum UI-Thread mache. Am Ende der Testmethode wird der ObjectThread wieder gestoppt und alles ist aufgeräumt. Aktuell sieht das Ergebnis auch genauso aus, wie ich es brauche.

Verweise

  1. …master/Tests/SeeingSharp.Tests.Rendering/ErrorHandlingTests.cs

Schreibe einen Kommentar

Diese Website verwendet Akismet, um Spam zu reduzieren. Erfahre mehr darüber, wie deine Kommentardaten verarbeitet werden.