|
Testkonzept
Testkonzept: Flexible Betreuung FH-Frankfurt
EinleitungZiel dieses Konzeptes ist es, einen Leitfaden zur Verfügung zu stellen, der bei der Erstellung und Ausführung von dynamischen Tests hilft. TestumfeldBei 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 phpunitPhpunit 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:
In der Eingabeaufforderung folgende Befehle absetzen:
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 NetbeansNetbeans 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/UnittestDer 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 TestsDas 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. SyntaxEin 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 MockingMocking 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 MockingBetrachten 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 IntegrationstestBei 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. SystemtestDas 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:
Ebenso bei der Übergabe von nicht validen Daten: Beispiel für negative tests:
Beispiel an der Klasse: PersonBetrachteten 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:
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:
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());
}
}
|