Schmerzfreier Einsatz von UnitTests

| 8 Kommentare

Bereits vor kanpp zwei Jahren habe ich schonmal etwas zum Thema Magento und UnitTests geschrieben. Inzwischen arbeite ich mit Symfony2 und stellte mit Freude fest, dass automatisiertes Testing damit wesentlich einfacher funktioniert. Dependency Injection sei dank!

Trotzdem müssen wir nach wie vor die Tests selber schreiben und gerade, wenn das nachträglich passiert, kann das schon eine Menge Arbeit machen. Aber müssen wir denn für jede Methode ausführliche Tests schreiben? Ich behaupte, dass wir uns das sparen können.

Natürlich wollen wir mit Tests unsere Anwendung möglichst wasserdicht machen und Schwachstellen sofort aufdecken. Wie ich schon im ersten Artikel beschrieben habe, ist es recht einfach und sinnvoll, eigene TestCases abzuleiten und mit “Macro-Methoden” zu versehen. Diese Methoden fassen ganz einfach mehrere andere Methoden zusammen und sparen später viel Tipp- oder Kopierarbeit.

<?php
namespace mein\project\testing;

class TestCase extends \PHPUnit_Framework_TestCase {

  public function assertMethodExists(
    $object, $method, $message = null) {
    if(null === $message) {
      $message = 'Methode '
        . get_class($object) . '::'
        . $method .'() existiert nicht.';
      }
      $this->assertTrue( method_exists($object, $method), $message);
  }

  public function assertGetterAndSetter($object, $property, $value = null) {
    if(null === $value) {
      $value = md5(rand0, 999999);
    }

    $getter = 'get' . $property;
    $setter = 'set' . $property;

    $this->assertMethodExists($object, $getter);
    $this->assertMethodExists($object, $setter);
    $this->assertEquals($object, $object->$setter($value)
      "$setter() bietet kein fluent interface");
    $this->assertEquals($value, $object->$getter(),
      "$getter() gibt unerwarteten Wert zurück");
  }
}

Mit der Methode assertGetterAndSetter() können wir nun schnell alle Klassen absichern, die ihre Eigenschafen über Zugriffsmethoden zur Verfügung stellen. Dabei werden mit einem Aufruf vier Bedingungen überprüft: Existiert ein Getter, existiert ein Setter, unterstütz der Setter Fluent Interface (Rückgabewert ist das Objekt) und bekommen wir vom Getter den Wert wieder, dem wir zuvor dem Setter gegeben haben.

In einer Testklasse sind das dann nur noch wenige Zeilen Code.

<?php
class PersonTest extends TestCase {
  public function testSetAndGetId() {
    $this->assertGetterAndSetter($this->createInstance(), 'id');
  }

  public function testSetAndGetFirstName() {
    $this->assertGetterAndSetter($this->createInstance(), 'firstName');
  }

  public function testSetAndGetLastName() {
    $this->assertGetterAndSetter($this->createInstance(), 'lastName');
  }
}

Natürlich können wir in unseren Klassen soviele eigene Asserts anlegen wie wir wollen und damit alle unsere Standardtest zusammenfassen.

Die zweite Möglichkeit die Tests einfach zu erweitern wäre die Vererbungshierachie der Testklassen parallel zu der zu testenden Klassen aufzubauen und dort bereits die Testmethoden zu schreiben. Die Tests würden dann für jede vererbte Klasse ausgeführt werden und sichern damit, dass das gewünschte Verhalten in keiner Kindklasse falsch überschrieben wird.

Erstmal die zu testenden Klassen:

<?php
class Model {
  public function getId() {}
  public function setId($id) {}
}

class Person extends Model {
  public function getName() {}
  public function setName($name) {}
}

class Employee extends Person {
  public function getSalary() {}
  public function setSalary($salary) {}
}

Und dann die Testklassen:

<?php
class ModelTestCase extends TestCase {
  public function testSetAndGetId() {
    $this->assertGetterAndSetter($this->createInstance(), 'id');
  }
}

class PersonTestCase extends ModelTestCase {
  public function testSetAndGetName() {
    $this->assertGetterAndSetter($this->createInstance(), 'name');
  }
}

class EmployeeTest extends PersonTestCase {
  public function testSetAndGetSalary() {
    $this->assertGetterAndSetter($this->createInstance(), 'salary');
  }
}

So lässt sich eine Menge Arbeit sparen und auch Erweiterungen an den Klassen und Test gehen dann schnell.

8 Kommentare

  1. Dass es Sinn macht die Testklassen zu erweitern denke ich auch. Das sollte man auf jeden Fall machen, denn auch beim Testen sollte man gutes Softwaredesign beachten und duplizierten Code etc. vermeiden.

    Das mit der Vererbung finde ich allerdings eher unschön, da es die Fehlersuche im Falle eines fehlgeschlagenen Tests stark negativ beeinflussen kann, da beispielsweise mehrere Konstruktoren aufgerufen werden und dann auch nicht mehr gewährleistet ist, dass z.B. eine Methode isoliert getestet wird.

  2. Danke für deinen Kommentar. Wie würdest du das ohne Vererbung lösen wollen?

  3. Was genau möchstest du denn testen? In deinem Beispiel erreichst du durch die Vererbung ja nichts anderes, als dass u.U. das Testinventar mehrmals geladen wird. Es macht meiner Meinung nach an dieser Stelle keinen Sinn die Tesklassen voneinander erben zu lassen.

    Generell testest du mit Unit Tests ja kleine, für sich isolierte, Einheiten. Das heisst, dass du in “EmployeeTest” die Methoden/Funktionalität der Klasse “Employee” testest. Du möchtest sicherstellen, dass jede Methode das tut, was sie soll. Du gibst dabei in den Assertions an, welches Ergebnis du erwartest. Das Ergebnis sollte nicht von anderen Methoden abhängen.

  4. Der Modeltest testet get/setId(), der Test für Person testet auch die Id und die Getter und Setter für den Namen und der EmployeeTest macht alles das auch und zusätzlich den Test für get/setSalery(). Dadurch kannst du alles für sich testen und stellst sicher, dass auch kein Fehler in einer überschriebenen Methode auftritt.

    Wenn die Employee-Klasse z.B. eine Methode setName() hätte, würde diese erstmal nicht automatisch getestet werden. Nun kannst du sichergehen, das die Schnittstelle immer sauber ist.

  5. Das bedeutet, dass du ein und denselben Test (z.B. Get/SetId) 3 mal ausführst, obwohl du schon beim ersten Test ein eindeutiges ergebnis hast. Ob get/setId richtig funktioniert, sollte dich beim Testen von get/setName nicht interessieren.

  6. Das ist richtig. Jedes Getter/Setter-Paar wird für sich getestet, aber auf allen Objekten. Es hilft ja nichts, wenn setName() der Personenklasse fluent interface unterstützt und in der überschriebenen Methode in der Employeeklasse hat jemand das “return $this” vergessen.

  7. Ich glaube jetzt verstehe ich, was du meinst. Aber der Ansatz ist meiner Meinung nach trotzdem mit unnötigem Overhead verbunden.

    Beispiel:
    class Foo
    {
    protected $bar;

    public function setBar($bar)
    {
    $this->bar = $bar;
    }
    }

    class Baz exends Foo
    {
    protected $baz;

    public function setBaz($baz)
    {
    $this->baz = $baz;
    }
    }

    Testen sollte man nun einmal die Klasse Foo mit dem Testcase “testSetBar” und einmal die Klasse Baz mit dem Testcase “testSetBaz”. Es ist unnötig in der Testklasse der Baz Klasse die setBar Methode zu prüfen, da:
    1. die Methode schon im Testcase der Klasse Foo getestet wurde.
    2. es an dieser Stelle keine Rolle für den Test spielen darf, ob die Methode einer anderen Klasse funktioniert, da Methoden isoliert getestet werden.

    Die Baz Klasse braucht den Testcase “testSetBar” nur, wenn die Methode tatsächlich erweitert/überschrieben werden würde. Solange das nicht der Fall ist, braucht und sollte man die Methode dort an der Stelle nicht testen, weil man dann unnötigerweise die Methoden einer anderen Klasse testet, die eigentlich für den Test nicht relevant sind.

  8. Du schreibst “Aber müssen wir denn für jede Methode ausführliche Tests schreiben? Ich behaupte, dass wir uns das sparen können.” – genau so sehe ich es auch, daher sind Trivialmethoden wie Getter und Setter bei mir normalerweise nicht von Tests abgedeckt.

    Zugegeben, um Fluid Interface sicherzustellen kann es ganz sinnvoll sein, “return $this” ist bei selbstgeschriebenen Settern schnell mal vergessen. Dann lässt sich dein Code sogar noch weiter vereinfachen, Stichwort Data Provider:
    /**
    * @test
    * @dataProvider dataProperties
    */
    public function testAccessors($property, $value = null)
    {
    $this->assertGetterAndSetter($this->subject, $property, $value
    }
    public function dataProperties()
    {
    return array(
    array('firstName', 'Max'),
    array('lastName', 'Mustermann'),
    array('salary', 40000)
    );
    }

Hinterlasse eine Antwort

Pflichtfelder sind mit * markiert.


Schlagwörter: A/B-Test, AbstractType, Adapter, AddOn, Administration, Ajax, Amazon, Animation, Annotations, Anonyme Klasse, Ant, Apache, API, Array, ArrayAccess, Attachment, Auftrag, Ausbildung, Auswertung, Authentifizierung, AutoLoader, AWS, Bedienung, Bedingung, Benchmark, Berechtigung, Berlin, Bildbearbeitung, Bildschirmfoto, Blog, Blogroll, BOM, Bootstrap, Bot, Browser, Bugtracker, Byte Order Mark, Bücher, Cache, CakePHP, Call-Center, Callback, CamelCase, Canvas, Captcha, CDN, Cheatsheet, CLI, Clickout, Closure, Cloud, CodeSniffer, Collection, Community, Comparator, Config, Contest, Controller, Converter, CouchDB, Countable, Cronjob, CRUD, CSS, CSV, CustomLibrary, Custom_Model, Daemon, Data Mapper, Datei, Datenbank, Datenstruktur, Datentypen, Dating, Datum, Debug, Decorator, Dekorierer, Design, Design Patterns, Doctrine, Dokumentation, Dump, Duplikat, each, EC2, Eclipse, Email, Entwicklung, Entwurfsmuster, Enum, Erweiterung, Event, Eventhandling, Exception-Handling, Extension, Facebook, Factory, Fallback, Fehler, Fehlermeldung, Filter, Firefox, Flash, flexigrid, Foreach, Formatierung, Formular, Framework, FTP, Funktion, Futon, ga:pi(), Getter, Google Analytics, Hash, Hash-Bang, Header, htaccess, HTML5, htpasswd, HTTP, HTTPS, IDE, If, Implementierung, InnoDB, Interceptor, Interface, Internet Explorer, isset, Iterator, Java, JavaScript, Job, jQuery, Kommentar, Konfiguration, Konsole, Kontrollstruktur, kopieren, kostenlos, Kundenbetreuung, Late Static Binding, Layout, Links, Linux, Listeners, Lizenz, Logging, Löschen, Magento, Magic Methods, Manual, ManyToMany, Marketing, Methode, Model, Monolog, MVC, MySQL, NetBeans, Network, Nirvanix, Objekt, Observable, Observer, OneToMany, Online Tool, OOP, Open Source, Operator, OR-Mapper, Order, ORM, O’Reilly, Parameter, Partnersuche, Passwort, Performance, PHP, php.ini, PHP hates me, phpMyAdmin, PHPUnit, Plugin, Popup, Proxy, Prüfsumme, Prüfung, QR-Code, Qualitätssicherung, Query, Queue, Redesign, Refactoring, Reflection, Request, Response, Responsive Design, Rest-API, Rockstar, Rollback, Routing, S3, Samba, Scheifen, Schleife, Schutz, Screenshot, Secure Shell, Selbstreferenz, Server, Setter, setTimeout, Shop, Sicherheit, Sicherung, Sichtbarkeit, Singleton Pattern, Skin, SOAP, Social Network, Software, Sortierung, Sourcecode, Spam, Speicherproblem, Spickzettel, SPL, Splittest, SSH, SSL, Stammtisch, Statement, static, Statistik, Status, Stellvertreter, Strategy Pattern, Stream, String, Stuttgart, Stylesheet, Subversion, Sun VirtualBox, Support, SVN, Switch, Symfony, Symfony2, Symfony Live, Tag, Template, Template Method, Ternär Operator, Testing, Theme, Thumbnail, Tool, Tour, Tracking, Twig, Twitter, Type-Cast, Ubuntu, Umwandlung, Underscore, unset, Update, Upload, Url, User Story, Validierung, Vererbung, Versionskontrolle, Versionsnummer, Verzweigung, Video, Videospiel, Virtualisierung, Visitor Pattern, Vorschaubild, walk, Warteschlange, Webserver, Webservice, Weiterleitung, Werkzeug, Windows, WindowsAzure, WordPress, Wrapper, Writer, XML, Youtube, Zeitschleife, Zeitsteuerung, Zend Framework, Zend_Application, Zend_Cloud, Zend_CodeGenerator, Zend_Http_Client, Zend_Reflection, Zend_Service, ZPress, Zugangskontrolle, Zugriffsmethode