PHPUnit - Code-Coverage und Komplexität ermitteln

25. November 2013 Softwaretest von Eric Kubenka

Gerade für mich als Softwaretester nimmt der Testprozess einer Anwendung einen bedeutenden Stellenwert ein. Stetig rückt in der heutigen Zeit das Test-Driven-Development, sowie die damit verbundenen Vor- und Nachteile in den Vordergrund. Aber auch das Testen allgemein hält immer mehr Einzug. Auf Komponententestbasis bietet es sich an maschinell prüfen zu lassen, welche Codeteile bereits durch geschriebene Testfälle abgedeckt werden.

Code Coverage Projekt

Große IDEs bringen solche Tools natürlich von Haus aus mit, aber auch ohne große IDEs lässt sich bei PHP Projekten die Codeabdeckung überprüfen. Dank des enthaltenen Code-Coverage-Analysetools in PHPUnit, lässt sich schnell und effektiv ermitteln, welche Klassen noch besser getestet werden müssen, aber auch welche Klassen eine zu hohe Komplexität besitzen. 

Nachdem nun bereits kurz skizziert wurde, was unter dem Begriff Code-Coverage, beziehungsweise Codeabdeckung zu verstehen ist, geht es nun direkt ins Beispiel. Ich verwende priorisiert folgende Ordnerstruktur für meine PHP-Projekte und nutze wie fast immer Composer. 

src
    Comparer.php
test
    ComparerTest.php
vendor
    Abhängigkeiten / Dependencies

Diese Ordnerstruktur ermöglicht einen gewissen Überblick. Tests sind exakt nach den zu testenden Komponenten benannt und tragen das Suffix "Test". Weiterhin wird das Anlegen von Guards ebenfalls erleichtert. Somit kann schnell beim OnSave in einer src-File die dazugehörige Testklasse ausgeführt werden.

Folgender Beispielcode (auch auf Github, Link am Ende des Artikels) dient für den Rest des Posts als Referenzbeispiel.

Da PHPUnit verwendet wird, wäre es ratsam eine entsprechende phpunit.xml-Datei im Projektroot bereitzustellen.

<?php 

class Comparer
{
	public function __construct()
	{

	}

	public function compare($one, $two)
	{
		if($one < $two)
		{
			return 'one < two';
		}

		if($one > $two)
		{
			return 'one > two';
		}

		if($one === $two)
		{
			return 'one === two';
		}

		return 'something else';
	}
}
<?xml version="1.0" encoding="UTF-8"?>
<phpunit backupGlobals="false"
         backupStaticAttributes="false"
         bootstrap="vendor/autoload.php"
         colors="true"
         convertErrorsToExceptions="true"
         convertNoticesToExceptions="true"
         convertWarningsToExceptions="true"
         processIsolation="false"
         stopOnFailure="false"
         syntaxCheck="false"
>
    <testsuites>
        <testsuite name="Application Test Suite">
            <directory>./tests/</directory>
            <exclude>./vendor/</exclude>
        </testsuite>
    </testsuites>
    <filter>
        <blacklist>
            <directory suffix='.php'>./vendor/</directory>
            <directory suffix='.php'>./tests/</directory>
        </blacklist>
    </filter>
</phpunit>

Lässt man diesen Code nun mit folgendem Befehl phpunit --coverage-html ausgehend vom Projektroot gegen die folgenden Tests laufen, so wird im Projektroot der Ordner coverage angelegt. In diesem befindet sich eine nette index.html-Datei, welche beim Öffnen im Browser eine hübsch aufbereitete Code-Coverage darbietet.

class ComparerTest extends PHPUnit_Framework_TestCase
{
	public function testCompareASmallerB()
	{
		$comparer = new Comparer();
		$result = $comparer->compare(12, 20);

		$this->assertEquals('one < two', $result);
	}

	public function testCompareBSmallerA()
	{
		$comparer = new Comparer();
		$result = $comparer->compare(25, 20);

		$this->assertEquals('one > two', $result);
	}

Comparer Testlauf 1

Zum einen der Überblick über alle Klassen, welche in der phpunit.xml in den zu testenden Ordnern lagen und zum anderen die direkte Auswertung der getesteten Comparer-Klasse. Wie wir selbst im Kopf noch verifizieren konnten, wird nicht jede Code-Zeile abgedeckt, also nicht jede Anweisung ausgeführt.

Direkte Klassenauswertung

 

Nach Hinzufügen folgender Testfälle, ergibt sich dann eine Abdeckung von 100%. Das lässt sich auch noch recht schnell im Kopf prüfen, bloß dient die Code-Coverage gerade in großen Projekten, bei denen selbst in kleinen Units eine hohe Komplexität vorliegt, zur Abdeckungs-Überprüfung.

public function testCompareWithEqual()
	{
		$comparer = new Comparer();
		$result = $comparer->compare(10, 10);

		$this->assertEquals('one === two', $result);
	}

	public function testCompareWithNonEqual()
	{
		$comparer = new Comparer();
		$result = $comparer->compare('10', 10);

		$this->assertEquals('something else', $result);
	}

Die Komplexität einer Klasse lässt sich durch diverse Metriken bestimmen. Eine davon ist die zyklomatische Zahl, welche angibt wie viele unabhängige Pfade durch eine Komponente/Unit oder ein Modul genommen werden können. Dabei ist ein hoher Wert ein Indiz für eine hohe Komplexität.

Die McCabe-Metrik (auch zyklomatische Komplexität – cyclomatic complexity) ist eine Software-Metrik, mit der die Komplexität eines Software-Moduls (Funktion, Prozedur oder allgemein ein Stück Sourcecode) gemessen werden kann. Die zyklomatische Komplexität wurde 1976 durch Thomas J. McCabe eingeführt.

Hinter der Software-Metrik von McCabe steckt der Gedanke, dass ab einer bestimmten Komplexität das Modul für den Menschen nicht mehr begreifbar ist. Die cyclomatic complexity ist definiert als Anzahl linear unabhängiger Pfade auf dem Kontrollflussgraphen eines Moduls. Damit ist die Zahl eine obere Schranke für die Anzahl der Testfälle, die nötig sind, um eine vollständige Zweigüberdeckung des Kontrollflussgraphen zu erreichen.

Wikipedia - 25.11.2013

Jedoch hat die vorliegende compare-Methode bereits eine Komplexität von 4, obwohl sie für uns Menschen doch recht einfach überschaubar ist. Also nicht immer sagt ein hoher Wert etwas über die tatsächliche Komplexität aus, zeigt aber, dass man sich den betroffenen Quellcode noch einmal anschauen sollte. Ein einfaches switch-Case für Wochentage kann bereits eine Komplexität von 8 erreichen.

PHPUnit bietet im Dashboard der Code-Coverage direkt eine Übersicht über die "schlimmsten Klassen". Also welche Klasse hat die geringste Abdeckung, welche Klasse hat die höchste Komplexität. All sowas lässt sich auf einem Blick ablesen und so lassen sich die Tests in eine entsprechende Richtugn steuern, beziehungsweise auf ein Modul konzentrieren.

Dashboard für Comparer Projekt

So wird selbst durch PHPUnit bereits schnell auf Projektrisiken hingewiesen und dem Testmanager, beziehungsweise dem Test-Analyst etwas Arbeit abgenommen :).

Das soll es auch schon zur Einführung gewesen sein. Ich hoffe das einfache Beispiel hat kurz und klar gezeigt, welche Boardmittel PHPUnit bereits mitbringt. Der Code ist wie immer auf Github clone-bereit :).

Zurück