Frameworks bieten ein Werkzeug für die schnelle Anwendungsentwicklung, bringen aber technische Schulden häufig so schnell auf, wie Sie Funktionalität erstellen können. Technische Schulden entstehen, wenn die Wartbarkeit kein zweckmäßiger Fokus des Entwicklers ist. Zukünftige Änderungen und Fehlerbehebungen werden aufgrund fehlender Komponententests und -strukturen teuer.
So beginnen Sie mit der Strukturierung Ihres Codes, um Testbarkeit und Wartbarkeit zu erreichen - und sparen Sie Zeit.
Beginnen wir mit einigem, aber typischem Code. Dies kann eine Modellklasse in einem gegebenen Rahmen sein.
Klasse User öffentliche Funktion getCurrentUser () $ user_id = $ _SESSION ['user_id']; $ user = App :: db-> select ('id, username') -> where ('id', $ user_id) -> limit (1) -> get (); if ($ user-> num_results ()> 0) return $ user-> row (); falsch zurückgeben;
Dieser Code wird funktionieren, muss aber verbessert werden:
$ _SESSION
Globale Variable. Unit-Testing-Frameworks wie PHPUnit hängen von der Befehlszeile ab, wo $ _SESSION
und viele andere globale Variablen sind nicht verfügbar.App :: db
in unserer Anwendung verwendet. Und wie sieht es mit Fällen aus, in denen wir nicht nur die aktuellen Benutzerinformationen wünschen?Hier ist ein Versuch, einen Komponententest für die oben genannte Funktionalität zu erstellen.
Klasse UserModelTest erweitert PHPUnit_Framework_TestCase public function testGetUser () $ user = new User (); $ currentUser = $ user-> getCurrentUser (); $ this-> assertEquals (1, $ currentUser-> id);
Lass uns das untersuchen. Erstens schlägt der Test fehl. Das $ _SESSION
Variable in der Nutzer
Objekt existiert nicht in einem Komponententest, da es PHP in der Befehlszeile ausführt.
Zweitens gibt es keine Datenbankverbindung. Dies bedeutet, dass wir, um dies funktionieren zu können, unsere Anwendung bootstrapen müssen, um die App
Objekt und seine db
Objekt. Zum Testen wird auch eine funktionierende Datenbankverbindung benötigt.
Damit dieser Gerätetest funktioniert, müssten wir:
Lassen Sie uns also darüber sprechen, wie wir das verbessern können.
Die Funktion zum Abrufen des aktuellen Benutzers ist in diesem einfachen Kontext nicht erforderlich. Dies ist ein konstruiertes Beispiel, aber im Sinne der DRY-Prinzipien möchte ich als erste Optimierung diese Methode verallgemeinern.
class User öffentliche Funktion getUser ($ user_id) $ user = App :: db-> select ('user') -> where ('id', $ user_id) -> limit (1) -> get (); if ($ user-> num_results ()> 0) return $ user-> row (); falsch zurückgeben;
Dies ist eine Methode, die wir für unsere gesamte Anwendung verwenden können. Wir können den aktuellen Benutzer zum Zeitpunkt des Aufrufs übergeben, anstatt diese Funktionalität an das Modell zu übergeben. Code ist modularer und wartungsfreundlicher, wenn er nicht auf andere Funktionen angewiesen ist (z. B. die globale Variable der Sitzung)..
Dies ist jedoch immer noch nicht testbar und wartbar, wie es sein könnte. Wir verlassen uns immer noch auf die Datenbankverbindung.
Lassen Sie uns helfen, die Situation zu verbessern, indem Sie Abhängigkeitsinjektion hinzufügen. So könnte unser Modell aussehen, wenn wir die Datenbankverbindung in die Klasse übergeben.
Klasse Benutzer protected $ _db; öffentliche Funktion __construct ($ db_connection) $ this -> _ db = $ db_connection; public function getUser ($ user_id) $ user = $ this -> _ db-> select ('user') -> where ('id', $ user_id) -> limit (1) -> get (); if ($ user-> num_results ()> 0) return $ user-> row (); falsch zurückgeben;
Nun, die Abhängigkeiten unserer Nutzer
Modell sind vorgesehen. Unsere Klasse geht nicht mehr von einer bestimmten Datenbankverbindung aus und ist nicht auf globale Objekte angewiesen.
An diesem Punkt ist unsere Klasse grundsätzlich testfähig. Wir können eine Datenquelle unserer Wahl (meistens) und eine Benutzer-ID übergeben und die Ergebnisse dieses Anrufs testen. Wir können auch separate Datenbankverbindungen austauschen (vorausgesetzt, beide implementieren dieselben Methoden zum Abrufen von Daten). Cool.
Schauen wir uns an, wie ein Unit-Test dafür aussehen könnte.
_mockDb (); $ user = neuer Benutzer ($ db_connection); $ result = $ user-> getUser (1); $ erwartet = new StdClass (); $ erwartet-> id = 1; $ erwartet-> Benutzername = 'fideloper'; $ this-> assertEquals ($ result-> id, $ erwartet-> id, 'Benutzer-ID wurde korrekt festgelegt'); $ this-> assertEquals ($ result-> Benutzername, $ erwartet-> Benutzername, 'Benutzername richtig festgelegt'); protected function _mockDb () // "Mock" (Stub) -Zeilenzeile Ergebnisobjekt $ returnResult = new StdClass (); $ returnResult-> id = 1; $ returnResult-> Benutzername = 'fideloper'; // Mock-Datenbankergebnisobjekt $ result = m :: mock ('DbResult'); $ result-> sollteReceive ('num_results') -> once () -> andReturn (1); $ result-> shouldReceive ('row') -> once () -> andReturn ($ returnResult); // Mock-Datenbankverbindungsobjekt $ db = m :: mock ('DbConnection'); $ db-> shouldReceive ('select') -> once () -> andReturn ($ db); $ db-> sollteReceive ('wo') -> einmal () -> andReturn ($ db); $ db-> shouldReceive ('limit') -> once () -> andReturn ($ db); $ db-> shouldReceive ('get') -> once () -> andReturn ($ result); return $ db;
Ich habe diesem Unit-Test etwas Neues hinzugefügt: Spott. Mit Mockery können Sie PHP-Objekte "nachahmen". In diesem Fall verspotten wir die Datenbankverbindung. Mit unserem Modell können wir das Testen einer Datenbankverbindung überspringen und unser Modell einfach testen.
Möchten Sie mehr über Mockery erfahren?
In diesem Fall verspotten wir eine SQL-Verbindung. Wir sagen dem Scheinobjekt, dass es das erwartet wählen
, woher
, Grenze
und erhalten
Methoden dazu aufgerufen. Ich gebe das Mock selbst zurück, um zu spiegeln, wie das SQL-Verbindungsobjekt sich selbst zurückgibt ($ das
), so dass ihre Methode "verkettbar" heißt. Beachten Sie, dass für die erhalten
Methode, ich gebe das Ergebnis des Datenbankaufrufs zurück - a stdClass
Objekt mit den Benutzerdaten gefüllt.
Dies löst einige Probleme:
Wir können noch viel besser machen. Hier wird es interessant.
Um dies weiter zu verbessern, könnten wir eine Schnittstelle definieren und implementieren. Betrachten Sie den folgenden Code.
Schnittstelle UserRepositoryInterface public function getUser ($ user_id); class MysqlUserRepository implementiert UserRepositoryInterface protected $ _db; öffentliche Funktion __construct ($ db_conn) $ this -> _ db = $ db_conn; public function getUser ($ user_id) $ user = $ this -> _ db-> select ('user') -> where ('id', $ user_id) -> limit (1) -> get (); if ($ user-> num_results ()> 0) return $ user-> row (); falsch zurückgeben; class User protected $ userStore; öffentliche Funktion __construct (UserRepositoryInterface $ user) $ this-> userStore = $ user; public function getUser ($ user_id) return $ this-> userStore-> getUser ($ user_id);
Hier passiert einiges.
Nutzer hinzufügen()
Methode.Benutzeroberfläche
in unserer Nutzer
Modell. Dies garantiert, dass die Datenquelle immer eine getUser ()
Methode verfügbar, unabhängig davon, welche Datenquelle zur Implementierung verwendet wird Benutzeroberfläche
.Beachten Sie, dass unsere
Nutzer
Objekttyp-HinweiseBenutzeroberfläche
in seinem Konstruktor. Dies bedeutet, dass eine Klasse implementiert wirdBenutzeroberfläche
MUSS in die übergeben werdenNutzer
Objekt. Dies ist eine Garantie, auf die wir uns verlassen - wir brauchen diegetUser
Methode immer verfügbar zu sein.
Was ist das Ergebnis davon??
Nutzer
Klasse können wir die Datenquelle leicht verspotten. (Das Testen der Implementierungen der Datenquelle wäre die Aufgabe eines separaten Komponententests.).Nutzer
Objekt, wenn wir müssen. Wenn Sie sich für SQL entscheiden, können Sie einfach eine andere Implementierung erstellen (z. B., MongoDbUser
) und gib das in dein Nutzer
Modell-.Wir haben auch unseren Unit-Test vereinfacht!
_mockUserRepo (); $ user = neuer Benutzer ($ userRepo); $ result = $ user-> getUser (1); $ erwartet = new StdClass (); $ erwartet-> id = 1; $ erwartet-> Benutzername = 'fideloper'; $ this-> assertEquals ($ result-> id, $ erwartet-> id, 'Benutzer-ID wurde korrekt festgelegt'); $ this-> assertEquals ($ result-> Benutzername, $ erwartet-> Benutzername, 'Benutzername richtig festgelegt'); protected function _mockUserRepo () // Schein erwartetes Ergebnis $ result = new StdClass (); $ result-> id = 1; $ result-> username = 'fideloper'; // Mock jedes Benutzer-Repository $ userRepo = m :: mock ('Fideloper \ Third \ Repository \ UserRepositoryInterface'); $ userRepo-> shouldReceive ('getUser') -> once () -> andReturn ($ result); return $ userRepo;
Wir haben die Arbeit, eine Datenbankverbindung zu verspotten, komplett erledigt. Stattdessen simulieren wir einfach die Datenquelle und sagen ihr, was wann zu tun ist getUser
wird genannt.
Aber wir können es noch besser machen!
Beachten Sie die Verwendung unseres aktuellen Codes:
// In einem Controller $ user = new User (neuer MysqlUser (App: db-> getConnection ("mysql"))); $ user-> id = App :: session ("benutzer-> id"); $ currentUser = $ user-> getUser ($ user_id);
Unser letzter Schritt wird die Einführung sein Behälter. Im obigen Code müssen wir eine Reihe von Objekten erstellen und verwenden, um unseren aktuellen Benutzer zu erreichen. Dieser Code kann in Ihrer Anwendung verstreut sein. Wenn Sie von MySQL zu MongoDB wechseln müssen, müssen Sie immer noch Sie müssen jeden Ort bearbeiten, an dem der obige Code angezeigt wird. Das ist kaum trocken. Container können das beheben.
Ein Container "enthält" einfach ein Objekt oder eine Funktionalität. Es ähnelt einer Registrierung in Ihrer Anwendung. Wir können einen Container verwenden, um automatisch einen neuen zu instanziieren Nutzer
Objekt mit allen benötigten Abhängigkeiten. Im Folgenden verwende ich Pimple, eine beliebte Containerklasse.
// Irgendwo in einer Konfigurationsdatei $ container = new Pimple (); $ container ["user"] = function () neuen Benutzer zurückgeben (neuer MysqlUser (App: db-> getConnection ('mysql'))); // Jetzt können wir in allen Controllern einfach schreiben: $ currentUser = $ container ['user'] -> getUser (App :: session ('user_id'));
Ich habe die Schaffung der verschoben Nutzer
Modell an einem Ort in der Anwendungskonfiguration. Als Ergebnis:
Nutzer
Das Objekt und der gewünschte Datenspeicher werden in unserer Anwendung an einem Ort definiert.Nutzer
Modell von MySQL zu einer anderen Datenquelle in EIN Standort. Dies ist wesentlich wartungsfreundlicher.Im Verlauf dieses Tutorials haben wir Folgendes erreicht:
Ich bin sicher, Sie haben bemerkt, dass wir im Hinblick auf Wartbarkeit und Testbarkeit viel mehr Code hinzugefügt haben. Ein starkes Argument kann gegen diese Implementierung sprechen: Wir erhöhen die Komplexität. In der Tat erfordert dies tiefere Kenntnisse des Codes, sowohl für den Hauptautor als auch für die Mitarbeiter eines Projekts.
Die Kosten für Erklärung und Verständnis werden jedoch durch das zusätzliche Gesamtgewicht bei weitem aufgewogen verringern in technischen Schulden.
Sie können einschließen Spott und PHPUnit einfach in Ihre Anwendung mit Composer. Fügen Sie diese zu Ihrem Abschnitt "required-dev" in Ihrem hinzu composer.json
Datei:
"required-dev": "Spott / Spott": "0.8. *", "phpunit / phpunit": "3.7. *"
Sie können dann Ihre Composer-basierten Abhängigkeiten mit den "dev" -Anforderungen installieren:
$ php composer.phar install --dev
Weitere Informationen zu Mockery, Composer und PHPUnit finden Sie hier auf Nettuts+.
Erwägen Sie für PHP die Verwendung von Laravel 4, da Container und andere hier beschriebene Konzepte in außergewöhnlichem Umfang verwendet werden.
Danke fürs Lesen!