new-Operator in PHP überladen…
…oder besser nicht? Mit der Extension test_helpers, die Sebastian Bergmann in seinem PHPUnit Channel veröffentlicht hat, ist es möglich, einzugreifen, wenn ein Objekt mit Hilfe des new-Operators erzeugt wird und stattdessen eine andere Klasse zu instanziieren. Das kann vor allem bei Tests nötig werden, wenn es die Architektur einer Anwendung nicht erlaubt, einzelne Klassen auszutauschen:
class NewsFeedGenerator {
public function generateFeed() {
$newsTable = new NewsEntryTable();
foreach ($newsTable->getEntries() as $entry) {
// hier wird der Newsfeed generiert.
}
}
}
Wenn Sie für diese Klasse einen Unit-Test schreiben möchten, der prüft, ob die Klasse einen korrekten Newsfeed generiert, dann sind sie im Test immer davon abhängig, dass sie wissen, welche Einträge aktuell in der Datenbank stehen. Alternativ könnten Sie natürlich auch sicherstellen, dass die Datenbank im Voraus mit den entsprechend erwarteten Einträgen gefüllt wird.
Ein Unittest sollte allerdings eine einzelne Komponente (in diesem Fall den Newsfeed-Generator) isoliert testen, damit der Test nicht fehl schlägt, bloss weil die Datenbank im Moment nicht verfügbar ist. Aus diesem Grund werden in Unit-Tests Mock-Objekte eingesetzt, die das gewünschte Verhalten simulieren. Ein Mock für die Klasse NewsEntryTable könnte ganz einfach implementiert werden:
class NewsEntryTableMock {
public function getEntries() {
return array(
array('id' => 1, 'title' => 'Title 1', 'desc' => 'Description 1'),
array('id' => 2, 'title' => 'Title 2', 'desc' => 'Description 2')
// etc.
);
}
}
Wenn Sie diese Klasse verwenden, um den Newsfeed zu erzeugen, können Sie sich sicher sein, dass Sie immer die selben Daten als Basis verwenden.
Doch wie bekommen Sie die Klasse NewsFeedGenerator dazu, diese Datenquelle zu nutzen?
Mit Hilfe der neuen Extension ist das ganz einfach, Sie ersetzen die Klasse NewsEntryTable durch NewsEntryTableMock:
function overload_callback($className) {
if ($className === 'NewsEntryTable') {
return 'NewsEntryTableMock';
}
return $className;
}
set_new_overload('overload_callback');
Wenn Sie nun eine Instanz von NewsEntryTable erzeugen möchten, dann erzeugt PHP stattdessen eine Instanz von NewsEntryTableMock und sie können Ihren Newsfeed-Generator isoliert testen. Problem gelöst, möchte man meinen.
Das Problem ist jedoch nicht, dass ihr Test nicht isoliert ablaufen kann, sondern dass Sie eine feste Kopplung zwischen NewsFeedGenerator und NewsEntryTable in Ihrer Applikation haben. Was passiert, wenn Sie die Datenquelle auch im Betrieb austauschen müssen, da die Newseinträge nicht mehr in einer Datenbank sondern einer XML- oder CSV-Datei gespeichert werden, oder sogar über einen Service von einer anderen Applikation abgefragt werden müssen? Mit der aktuellen Architektur haben Sie dazu keine Möglichkeiten. Mit dem Einsatz von Dependency Injection jedoch schon:
class NewsFeedGenerator {
protected $dataSource;
public function __construct(NewsDataSource $dataSource) {
$this->dataSource = $dataSource
}
public function generateFeed() {
foreach ($this->dataSource->getEntries() as $entry) {
// hier wird der Newsfeed generiert.
}
}
}
interface NewsDataSource {
public function getEntries();
}
class DBNewsDataSource implements NewsDataSource {
public function getEntries() {
// Aus der Datenbank lesen
}
}
$ds = new DBNewsDataSource();
$generator = new NewsFeedGenerator($ds);
Die beiden Klassen sind nun nicht mehr voneinander abhängig, NewsFeedGenerator hat nur noch eine Abhängigkeit auf das entsprechende Interface, nicht mehr auf die Implementierung. Sie können die Implementierung für den Test oder auch im Live-Betrieb austauschen, ohne dazu die Extension verwenden zu müssen.
Sie mögen Sich nun fragen, wo das Problem liegt, wenn Sie die Klasse durch den Einsatz der test_helpers Extension austauschen, da Sie feste Abhängigkeiten haben. Ein Test für schlecht designten Code ist sicher besser als kein Test für schlecht designten Code. Das hängt jedoch davon ab, was man sich von Tests verspricht. Ich war noch nie ein großer Freund von Unit-Tests, es langweilt mich meistens, Tests zu schreiben. Was mich jedoch dazu gebracht hat, trotzdem Tests zu schreiben, ist, dass ich festgestellt habe, dass die Architektur der Applikation besser wird. Tests sind ein zweiter Client für die Applikation, der uns zwingt, die Applikation aus einem anderen Blickwinkel zu sehen. Durch Tests deckt man Abhängigkeiten auf und wird gezwungen, diese zu lösen. Die ganze Applikation wird loser gekoppelt und einzelne Komponenten leichtegwichtiger.
Das Risiko, das ich also beim Einsatz dieser Extension sehe, ist, dass man nicht mehr gezwungen wird, über die Kopplung nachzudenken. Damit geht ein großer Vorteil von Unit-Tests verloren. Sofern Sie sich sicher sind, dass eine Entkopplung zweier Klassen keine weiteren Vorteile (oder sogar Nachteile) bringt, sollte einem Einsatz der Extension nichts mehr im Wege stehen.
Wenn Sie also test-helpers einsetzen möchten, dann denken Sie jedes Mal darüber nach, ob Sie nicht ein grundlegendes Problem mit Abhängigkeiten haben und besser Ihre Applikation refaktorisieren sollten.
Xing
LinkedIn
Twitter
Ohloh
Slideshare
Facebook
Delicious
Github
Technorati
Flickr
Last.fm
YouTube
Amazon Wishlist