My favorites | Sign in
Project Home Downloads Wiki Issues Source
READ-ONLY: This project has been archived. For more information see this post.
Search
for
Testkonzept  
Testkonzept: Flexible Betreuung FH-Frankfurt
Updated Nov 19, 2012 by andreas....@gmail.com

Einleitung

Ziel dieses Konzeptes ist es, einen Leitfaden zur Verfügung zu stellen, der bei der Erstellung und Ausführung von dynamischen Tests hilft.

Testumfeld

Bei dem Testumfeld in dem wir uns befinden handelt es sich um eine Php Zend-Framework-1.2 Webapplikation. Als Testframework kommt PHPUnit-3.7.8 zum Einsatz, dass von der IDE Netbeans aus gestartet wird. Zusätzlich kommen eigene Test-Klassen aus dem ZEND-Framework zum Einsatz, mit deren Hilfe effektiv Zend_Controller getestet werden können.

Installationsanleitung phpunit

Phpunit wird entwickelt von Sebastian Bergemann. Es existiert ein Repository auf github. Dort befindet sich eine Installationsanleitung mittels php-pear.

Wer die bereitgestellte VM benutzt bitte folgendes beachten:

  • Eingabeaufforderung "als Administrator ausführen"

In der Eingabeaufforderung folgende Befehle absetzen:

  • pear channel-discover pear.phpunit.de
  • pear install phpunit/PHPUnit_Selenium-1.0.1
  • pear install phpunit/Text_Template-1.1.4
  • pear install phpunit/PHPUnit_MockObject-1.0.3
  • pear install phpunit/DbUnit-1.0.0
  • pear install phpunit/File_Iterator-1.2.3
  • pear install phpunit/PHP_CodeCoverage-1.0.2
  • pear install phpunit/PHPUnit-3.5.15
  • pear channel-discover components.ez.no
  • pear install phpunit/PHPUnit_SkeletonGenerator

Bei Problemen oder Fehlermeldungen ggf. die Befehle "pear clear-cache" bzw. "pear update-channels" ausführen.

Danach kann die Installation mit dem Befehl "pear list -c phpunit" überprüft werden.

Am Ende sollte das Ergebnis so sein, wie in der Liste aufgezeigt.

Einrichtung von Netbeans

Netbeans muss für die Verwendung mit phpunit eingerichtet werden. Es wird davon ausgegangen, dass phpunit in der Version 3.7.8 installiert ist.

Dazu begibt man sich in den Reiter "Tools->Optionen". Unter dem Tab "Unit Testing" muss der Pfad zum phpunit command-line tool angegeben werden.

Zusätzlich zu phpunit empfiehlt sich die Installation der Erweiterung "phpunit skeleton generator". Diese stellt ein cli-skript zur Verfügung welches man unter "Skeleton Generator Script" unter Angabe des absoluten Pfads eintragen kann. Es ermöglicht im weiteren Verlauf die automatische Erstellung von TestCases mit der Möglichkeit einer Angabe von Erwartungswerten als Annotations (siehe Beispiel unter Modultest).

Hat man das getan kann man über "Run/Test Project" alle Tests seines ZEND Projektes ausführen. Über die Netbeans View "Test Results" erhält man nähere Informationen zu den Testresultaten.

Modultest/Unittest

Der Modultest oder auch Komponentenen- bzw. Unitest wird von dem jeweiligen Entwickler selbst durchgeführt. Es handelt sich hierbei um die kleinste zu testende Einheit. Dazu wird das ”White-Box-Testverfahren“ angewandt, d.h. der Test erfolgt unter Kenntnis des Programmcodes / der Programm- struktur. Daneben hat der Entwickler für die Wartbarkeit bzw. Übertragbarkeit des Programmcodes zu sorgen.

Erzeugen von Tests

Das Erstellen von Tests mit Netbeans ist sehr komfortabel. Wir betrachten folgendes Modul.

<?php
class Calculator {
    public function add($number1,$number2){
        return $number1+$number2;
    }
}
?>

Bevor wir eine Testklasse erzeugen, können wir mittels Annotations unsere Erwartungshaltung angeben.

<?php
class Calculator {
/**    
     * @assert (0, 0) == 0
     * @assert (0, 1) == 1
**/    
    public function add($number1,$number2){
        return $number1+$number2;
    }
}
?>

Mit einem Rechtsklick auf die Klasse über "Tools->Create PhpUnit Tests" wird im Testordner eine Testklasse für "Calculator" erzeugt.

<?php

/**
 * Generated by PHPUnit_SkeletonGenerator 1.2.0 on 2012-11-06 at 00:48:41.
 */
class CalculatorTest extends PHPUnit_Framework_TestCase {

    /**
     * @var Calculator
     */
    protected $object;

    /**
     * Sets up the fixture, for example, opens a network connection.
     * This method is called before a test is executed.
     */
    protected function setUp() {
        $this->object = new Calculator;
    }

    /**
     * Tears down the fixture, for example, closes a network connection.
     * This method is called after a test is executed.
     */
    protected function tearDown() {
        
    }

    /**
     * Generated from @assert (0, 0) == 0.
     *
     * @covers Calculator::add
     */
    public function testAdd() {
        $this->assertEquals(
                0, $this->object->add(0, 0)
        );
    }

    /**
     * Generated from @assert (0, 1) == 1.
     *
     * @covers Calculator::add
     */
    public function testAdd2() {
        $this->assertEquals(
                1, $this->object->add(0, 1)
        );
    }

}

Anzumerken sei an dieser Stelle, dass automatisch Tests erzeugt wurden die sich in der zuvor eingetragenen Annotations wiederspiegeln. Dabei wird der Entwickler darüber informiert auf welcher Annotation der Test basiert und welche Funktion der Klasse der Test abdeckt.

Weitere Informationen zu Annotations des Phpunit Skeleton Generators findet man hier

Sicherlich ist das generieren von Tests mit Hilfe von Annotations in seinem Funktionsumfang beschränkt, es bietet jedoch eine sehr gute Grundlage auf der sich aufbauen lässt.

Syntax

Ein Testcase ist eine Klasse die von PHPUnit_Framework_TestCase erbt. Der Name ergibt sich aus der zu testenden Klasse (Unit) und dem Postfix "Test"

FooBar -> FooBarTest

Ein TestCase kann mehrere Tests enthalten. Ein Test wird durch eine Methode dargestellt mit dem Namen der zu testenden Funktion und dem Prefix "test"

add() -> testAdd()

Der Prefix "test" ist in sofern wichtig als das phpunit nur solche Methoden als Test ansieht und ausführt.

Innerhalb eins Tests kommen Vergleiche oder "Assertions" zum Einsatz. Diese verifizieren innerhalb eines Tests das richtige Verhalten einer bestimmten Funktion. Sie werden von der Klasse "PHPUnit_Framework_TestCase" zur Verfügung gestellt. Eine Übersicht der möglichen Assertions erhält man hier

Mocking

Mocking ist ein Verfahren beim Testen von Software das es ermöglicht, Attrappen von Klassen und Methoden zu erstellen. Mock-Objekte erwecken von außen den Eindruck, dass es sich um "normale" Instanzen einer bestimmten Klasse handelt. In Wahrheit sind sie jedoch "leer" und das ausführen von Methoden bewirkt erst einmal nichts. Der Entwickler hat jedoch die Möglichkeit zu bestimmen, wie sich die Methoden des Mock-Objektes verhalten indem man z.b. bestimmten Methoden einen statischen Rückgabewert zuweist. Die Mocking Methode kommt in der Regel während des Modultests zum Einsatz und beseitigt de facto Abhängigkeiten zu anderen Klassen während den Tests.

Phpunit stellt bereits ein Mocking Framework zur Verfügung.

Beispiel Mocking

Betrachten wir für ein Beispiel folgende neue Klasse Human.php

<?php

class Human {
    private $calculator = null;
    
    /**
     * @assert (1,1) == 2
     */
    public function add($number1, $number2){
        if ($this->calculator == null){
            // Human thinks
            sleep(5);
            return $number1 + $number2;
        }else{
            return $this->calculator->add($number1,$number2);
        }
    }
    
    public function giveCalculator(Calculator $calculator){
        $this->calculator = $calculator;
    }
}

?>

Ein Mensch kann auch rechnen, benötigt dafür jedoch mehr Zeit. Deswegen kann man ihm einen Taschenrechner an die Hand geben, der das ganze beschleunigt. Wenn wir die Unit "Human" testen dann geht es uns nicht darum zu verifizieren ob der Taschenrechner richtig arbeitet, sonder lediglich ob der Mensch ihn auch benutzt wenn er ihn hat. Das der Taschenrechner funktioniert haben wir bereites im "CalculatorTest" bewiesen und darauf können wir uns verlassen. Nachdem mit Hilfe von Netbeans den TestCase "HumanTest" erstellt haben sieht das wie folgt aus (gezeigt wird nur die Test Methode um die es geht):

    public function testAdd() {
        $this->assertEquals(
                2, $this->object->add(1, 1)
        );
    }

Damit würde mann beweisen, dass der Mensch richtig rechnet. Jetzt müssen wir nur noch wissen, ob der Mensch den Taschenrechner zur Hilfe nimmt, wenn er einen hat. Dazu arbeiten wir mit einem Mock Objekt der Klasse Taschenrechner

    public function testAdd() {
        $this->assertEquals(
                2, $this->object->add(1, 1)
        );
        
        // Wir bauen eine Attrappe der Klasse Calculator und übergeben diese dem Menschen
        $calculator = $this->getMock('Calculator');
        $this->object->giveCalculator($calculator);
        
        // Unsere Erwartung ist, dass der Mensch den Taschenrechner benutzt und mit ihmr 2+2 rechnet
        $calculator->expects($this->once())->method('add')->with(2,2);
        
        // Wir lassen den Menschen wieder rechnen
        $this->object->add(2, 2);
            
    }

Eine gute Anlaufstelle für mehr Informationen zum Mocking Framework von Phpunit findet man hier

Integrationstest

Bei diesem Test werden zuvor die entstanden Module/Komponenten zu einer Einheit zusammengefügt. Ziel des Integrationstest ist es, Schnittstellenfehler bzw. falsche Schnittstellenformate aufzudecken. Dabei kann es sich um inkopatible Schnittstellenformate oder Protokollfehler handeln, d.h. eine Komponente übermittelt keine oder falsche Daten.

Systemtest

Das komplette System zusammen wird das erste mal während des Systemtests getestet. Dabei wird aus der Sicht der Kunden bzw. der späteren Anwender getestet. Es wird validiert, ob das System die gestellten Anforderungen vollständig und angemessen erfüllt.

Testfälle Unittest (Modultest)

Bei der Erstellung eines Unittests ist darauf zu achten, dass man positive und negative Fälle abdeckt. Dabei übergibt man einer Funktion richtige sowie falsche Daten. Wichtig ist dabei die Erwartungshaltung. Bei der Übergabe von validen Daten erwarte ich, dass eine Methode mir das in einer bestimmten Art und Weise mitteilt.

Beispiel für positive tests:

  • Bei Funktionen mit Rückgabewert:
    • true
    • Eine Instanz des Typs des zu erwartenden Rückgabewertes
  • Bei Funktionen ohne Rückgabewert
    • keine Exception wird geworfen
    • Testen der Auswirkungen der methode (auf andere Units)

Ebenso bei der Übergabe von nicht validen Daten:

Beispiel für negative tests:

  • es wird der Rückgabewert: false erwartet
  • eine Exception wird erwartet

Beispiel an der Klasse: Person

Betrachteten wir einen Auschnitt aus der Klasse Person welche für die Tabelle person verantwortlich ist. Der Methode create() wird ein array mit den Daten der Person übergeben, die man in der Datenbank abspeichern möchte.

class Model_Person extends Model_Base
{
...
    /**
     * Database table name
     *
     * @var string
     */
    protected $name = "person";

    /**
     * Creates a person
     * @param array $person
     */
    public function create(array $person)
    {
	$this->db->insert($this->name, $person);
    }
...
}

Der positive Test sieht wie folgt aus:

  • Merke dir wie viele Personen in der Datenbank sind
  • Erstelle eine Person mit validen Daten
  • Merke dir wie viele Personen jetzt in der Datenbank sind
  • vergwissere dich, dass die Anzahl nach dem erstellen um 1 größer ist
  •     private static $PERSON_DATA = array(
    	'forename' => 'Jürgen',
    	'surname' => 'Klopp',
    	'birthday' => '2012-10-29',
    	'person_type' => 'Elternteil'
        );
    ...
        public function testCreate()
        {
    	$count1 = count($this->person->listAll());
    	$this->person->create($this::$PERSON_DATA);
    	$count2 = count($this->person->listAll());
    	$this->assertEquals(*$count1 + 1*, *$count2*);
        }

Für den negativen Test wäre es nun gut zu testen was passiert, wenn ich der create() methode folgende falsche Daten übergebe:

  • ein array mit einem Key für den es in der Datenbank keine Spalte gibt
  • eine Parameter der kein array ist
  • ein Parameter der NULL ist

Die Tests für diese Fälle sehen wie folgt aus:

    /**
     * @covers Model_Person::create
     * @expectedException InvalidArgumentException
     */
    public function testCreateWithUnknownKeyThrowsException()
    {
	$data = $this::$PERSON_DATA;
	$data['impossible key'] = 'juhu';
	$this->person->create($data);
    }

    /**
     * @covers Model_Person::create
     * @expectedException InvalidArgumentException
     */
    public function testCreateWithNoArrayParameterThrowsException()
    {
	$this->person->create(new DateTime);
    }

    /**
     * @covers Model_Person::create
     * @expectedException InvalidArgumentException
     */
    public function testCreateWithNullParameterThrowsException()
    {
	$this->person->create(NULL);
    }

Anders als im positiven Test spiegelt sich meine Erwartungshaltung nicht in Assert Statements wieder sonder wird über die Annotation @expectedException InvalidArgumentException definiert. Ich erwarte also, dass bei einem invaliden Parameter oder Argument diese Php Standard Exception geworfen wird. Führt man den Test aus schlagen diese erst einmal mit folgender Fehlermeldung fehl:

Model_PersonTest::testCreateWithUnknownKeyThrowsException()
Failed asserting that exception of type "Zend_Db_Statement_Exception" matches expected exception "InvalidArgumentException". Message was: "SQLSTATE[42S22]: Column not found: 1054 Unknown column 'impossible key' in 'field list'".

Model_PersonTest::testCreateWithNoArrayParameterThrowsException()
Failed asserting that exception of type "PHPUnit_Framework_Error" matches expected exception "InvalidArgumentException". Message was: "Argument 1 passed to Model_Person::create() must be an array, object given, called in /Users/johnnypark/Sites/FlexibleKinderbetreuung/tests/application/models/Model_PersonTest.php on line 82 and defined".

Model_PersonTest::testCreateWithNullParameterThrowsException()
Failed asserting that exception of type "PHPUnit_Framework_Error" matches expected exception "InvalidArgumentException". Message was: "Argument 1 passed to Model_Person::create() must be an array, null given, called in /Users/johnnypark/Sites/FlexibleKinderbetreuung/tests/application/models/Model_PersonTest.php on line 91 and defined".

Das bedeutet, dass eine Exception geworfen wurde, der Typ jedoch nicht der erwarteten Exception entspricht. Mit einem Blick auf die Implementierung wird schnell klar, dass es einer weiteren Implementierung bedarf:

    /**
     * Creates a person
     * @param array $person
     */
    public function create($person)
    {
	if (!is_array($person))
	{
	    throw new InvalidArgumentException("Argument is " . gettype($person));
	}

	try
	{
	    $this->db->insert($this->name, $person);
	} catch (Zend_Db_Statement_Exception $e)
	{
	    throw new InvalidArgumentException("Argument is " . gettype($person).". Nested message is: ".$e->getMessage());
	}
    }
Powered by Google Project Hosting