Damit Mock-Objekte die instanceof
Prüfungen im zu testenden Code passieren können, müssen sie Kinder der simulierten Klasse sein. Aber als ebendiese Kindklassen können sie keine finalen Methoden überschreiben. In diesem Blogbeitrag werde ich euch zeigen, wie ihr trotzdem sauber testen könnt.
Das Problem
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
// Eine Klasse mit finaler Methode class Job { final public function run() { /* ... */ } public function getResult() { /* ... */ } } // Irgendwo im Bootstrap-Prozess DI::set('job', new Job()); // Ein Controller, der seinen Job machen will class IndexController { public function indexAction() { $job = DI::get('job'); if ($job instanceof Job) { $job->run(); $this->view->setVar('result', $job->getResult()); } } } |
Wir verfrachten unsere Job-Klasse in einen Dependency Injector (DI
) und benutzen sie im weiteren Verlauf entsprechend. Ein Test für obigen Controller würde also so aussehen:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
class IndexControllerTest extends PHPUnit_Framework_TestCase { public function testIndexAction() { $jobMock = $this->getMock('Job'); $jobMock->expects($this->once())->method('getResult')->will($this->returnValue('foo')); DI::set('job', $jobMock); $class = new IndexController(); $class->indexAction(); // Hier prüfen wir dann ob result richtig in der View gesetzt wurde. } } |
Das funktioniert zwar, aber run()
wird aus der tatsächlichen Job-Klasse ausgeführt. Was zu unerwünschten Nebeneffekten führen kann. Außerdem können wir nicht überprüfen, dass run()
auch tatsächlich ausgeführt wird.
Interface bietet die Lösung
Praktischerweise können wir nicht nur konkrete Klassen mit getMock()
simulieren sondern auch ein Interface. Der Vorteil dabei ist, dass ein Interface per Definition keine finalen Methoden haben kann.
Wir erweitern also unseren Code um ein Interface und lassen implementieren es mit der Job-Klasse.
1 2 3 4 5 6 7 8 9 10 11 12 |
// Ein Interface für unsere Jobs interface IsJob { public function run(); public function getResult(); } // Eine Klasse mit finaler Methode class Job implements IsJob { final public function run() { /* ... */ } public function getResult() { /* ... */ } } |
Das ist komplett sauberes OOP und nicht irgendein Workaround. Die instanceof
Prüfung im Code muss noch entsprechend angepasst werden, sonst lässt sie die simulierte Klasse nicht mehr durch.
1 2 3 |
if ($job instanceof IsJob) { /* ... */ } |
Der Test nutzt für getMock()
jetzt das Interface und wir können zusätzlich prüfen, ob run()
auch aufgerufen wird.
1 2 3 4 5 6 7 8 9 10 11 12 |
public function testIndexAction() { $jobMock = $this->getMock('IsJob'); $jobMock->expects($this->once())->method('run'); $jobMock->expects($this->once())->method('getResult')->will($this->returnValue('foo')); DI::set('job', $jobMock); $class = new IndexController(); $class->indexAction(); // Hier prüfen wir dann ob result richtig in der View gesetzt wurde. } |
Aufwand für den Umbau
Natürlich sollte die Anwendung nicht für einen Test geändert werden. Wir würden also niemals eine protected
Methode public
machen nur um sie testen zu können. Wie ich oben aber schon erwähnt habe, ist ein Interface ganz normales OOP und auch ohne Test eine sinnvolle Erweiterung des Codes.
Ein Interface zu erstellen ist keine große Sache, mit einer guten IDE sogar nur ein paar Klicks. Die instanceof
Prüfungen müssen zunächst nicht geändert werden, die Anwendung läuft genau wie vorher. Wenn wir dann durch weitere Tests, zu einer entsprechenden Methode kommen, können wir die Änderung immer noch vornehmen.
Obiges gilt natürlich nur für Legacy-Projekte, die nachträglich mit Tests verbessert werden. Wer den Gedanken von TDD (test-driven-development) lebt und somit seine Tests vor der Anwendung schreibt, muss sich hier keine Gedanken machen.