UnitTests oder Ansätze der testgetriebenen Entwicklung werden aktuell häufig bei Usergroups oder Fachkonferenzen angesprochen. Erst diese Woche war ich auf einem Usergroup-Treffen in Nürnberg genau zu diesem Thema. Ich persönlich beschäftige mich damit schon etwas länger, habe testgetriebene Ansätze bereits mehrmals bei passenden Anwendungsfällen durchgeführt und war damit auch stets sehr zufrieden. Bei dem genannten Vortrag musste ich aber feststellen, dass sich das, was ich persönlich mit diesen Tests mache, mittlerweile schon teils deutlich von der Theorie unterscheidet. Hier in diesem Beitrag möchte ich entsprechend meine Sicht auf UnitTests und testgetriebene Ansätze im Allgemeinen beschreiben.Um ganz von vorne anzufangen: Ich verwende das in Visual Studio integrierte MSTest seit ca. 3-4 Jahren, vorher habe ich keinen einzigen automatisierten Test geschrieben. Ausgelöst wurde das ganze schlicht durch den Wunsch, gewisse Logiken nicht immer manuell neu testen zu müssen. Zudem habe ich an mir und auch bei Bekannten festgestellt, dass man sich gerne Testoberflächen bastelt, um damit neu entwickelte Logik-Bausteine zu testen. Diese Testoberflächen verschwinden aber gerne im Laufe der Entwicklung wieder oder werden aus Gründen der schlechten Usability einfach nicht mehr zum Nachtesten verwendet. UnitTests waren bzw. sind meiner Ansicht nach ein besserer Weg um genau das Gleiche zu erreichen. Vorteil: Die Testlogik bleibt im Projekt bestehen und kann jederzeit ausgeführt oder gar direkt in den Build-Prozess integriert werden.
Nun zum wesentlichen Unterschied, der sich in meiner Praxis durchgesetzt hat: Ich setze meine mit MSTest geschriebenen Tests meistens deutlich höher an, als es bei UnitTests eigentlich gedacht ist. Wenn ich von UnitTests spreche, meine ich deswegen in Wirklichkeit eher Komponententests oder gar kleine Integrationstests. Ein Beispiel ist im nachfolgenden Screenshot zu sehen. Hierbei handelt es sich um einen Ausschnitt aus dem Testexplorer von Visual Studio während ich die Projektmappe von SeeingSharp geöffnet habe. Schon an den Namen einer der Testmethoden „Load and Render AC Shaded Object“ erkennt man schon, dass es kein klassischer UnitTest ist.
Nachfolgend als Beispiel die Kodierung eines dieser Tests. Kurz gesagt passiert darin folgendes: Zuerst wird ein Software-Renderer angelegt (MemoryRenderTarget), danach wird die 3D-Kamera konfiguriert, eine 3D-Scene definiert (3D Modell geladen), gerendert und das gerenderte Bild wird mit einem Referenz-Bild verglichen. Ist die Abweichung zum Referenz-Bild geringer als 20%, so wird der Test als erfolgreich betrachtet. Ziel dieses Tests ist es einfach, das Laden und das korrekte 3D-Rendering in Bezug auf externe 3D-Modelle zu testen. Per Definition zu viel Logik für einen „UnitTest“, für mich aber definitiv das praktischste, was ich machen kann. Geht einer der Tests auf Rot, ist der entsprechende Fehler spätestens beim Betrachten des gerenderten Bildes i. d. R. auch sehr schnell gefunden, also sind tiefer ansetzende UnitTests auch gar nicht nötig.
[TestMethod] [TestCategory(TEST_CATEGORY)] public void LoadAndRenderACSingleSidedObject() { using (MemoryRenderTarget memRenderTarget = new MemoryRenderTarget(1024, 1024)) { memRenderTarget.ClearColor = Color4.CornflowerBlue; // Get and configure the camera PerspectiveCamera3D camera = memRenderTarget.Camera as PerspectiveCamera3D; camera.Position = new Vector3(-1.5f, 1.5f, -1.5f); camera.Target = new Vector3(1f, 0f, 1f); camera.UpdateCamera(); // Define scene memRenderTarget.Scene.ManipulateSceneAsync((manipulator) => { NamedOrGenericKey geoResource = manipulator.AddResource<GeometryResource>( () => new GeometryResource(ACFileLoader.ImportObjectType(Properties.Resources.ModelSingleSided))); var newObject = manipulator.AddGeneric(geoResource); }).Wait(MANIPULATE_WAIT_TIME); // Take screenshot GDI.Bitmap screenshot = memRenderTarget.RenderLoop.GetScreenshotGdiAsync().Result; //screenshot.DumpToDesktop("Blub"); // Calculate and check difference float diff = BitmapComparison.CalculatePercentageDifference( screenshot, Properties.Resources.ReferenceImageSingleSidedObject); Assert.IsTrue(diff < 0.2, "Difference to reference image is to big!"); } // Finishing checks Assert.IsTrue(GraphicsCore.Current.MainLoop.RegisteredRenderLoopCount == 0, "RenderLoops where not disposed correctly!"); }
So oder so ähnlich schauen die meisten meiner automatisierten Tests aus. In der Praxis hat sich dieses Vorgehen für mich auch mehr als bewährt. Auf einem ähnlichen Level gehe ich auch bei testgetriebenen Ansätzen vor. Zunächst definierte ich hier die Anforderung an neue Komponenten, in dem ich die Testmethoden definierte und auch bereits vollständig ausprogrammiere. Danach geht es an das Entwickeln der jeweiligen Komponente.
Ebenfalls Interessant
- Automatische Integrationstests mit ASP.NET Core
https://www.rolandk.de/wp-posts/2023/08/automatische-integrationstests-mit-asp-net-core/
Ich habe vor etwa 8 Jahren angefangen mich mit Unit-Testing zu beschäftigen. Damals noch in dem Glauben es für jede Klasse eine Test-Klasse geben muss. Vor einiger Zeit habe dann dieses Video gefunden: http://vimeo.com/68375232
Ian Cooper erklärt hier sehr anschaulich welches die eigentlichen Intentionen sind im ursprünglichen TDD ala Kent Beck. Seit diesem Zeitpunkt schreibe ich meist nur noch Tests für logische Kompontenen. Dabei entdeckte ich unglaubliche viele Vorteile gegenüber dem klassen-basierten Testen.