Kaders bieden een hulpmiddel voor een snelle ontwikkeling van toepassingen, maar bouwen vaak technische schulden op zo snel als ze u toestaan om functionaliteit te creëren. Technische schuld wordt gecreëerd wanneer onderhoudbaarheid geen doelgericht aandachtspunt van de ontwikkelaar is. Toekomstige wijzigingen en debugging worden kostbaar door een gebrek aan unit-testing en structuur.
Hier leest u hoe u begint met het structureren van uw code om testbaarheid en onderhoudbaarheid te bereiken - en u tijd kunt besparen.
Laten we beginnen met een beetje gekunstelde, maar typische code. Dit kan een modelklasse zijn in een bepaald kader.
class Gebruiker public function getCurrentUser () $ user_id = $ _SESSION ['user_id']; $ user = App :: db-> select ('id, gebruikersnaam') -> where ('id', $ user_id) -> limit (1) -> get (); if ($ user-> num_results ()> 0) return $ user-> row (); return false;
Deze code werkt, maar moet worden verbeterd:
$ _SESSION
globale variabele. Unit-testing frameworks, zoals PHPUnit, vertrouwen op de opdrachtregel, waar $ _SESSION
en veel andere globale variabelen zijn niet beschikbaar.App :: db
gebruikt in onze applicatie. En hoe zit het met instanties waar we niet alleen de informatie van de huidige gebruiker willen hebben?Hier is een poging om een eenheidscontrole voor de bovenstaande functionaliteit te maken.
class UserModelTest breidt PHPUnit_Framework_TestCase uit public function testGetUser () $ user = new User (); $ currentUser = $ user-> getCurrentUser (); $ this-> assertEquals (1, $ currentUser-> id);
Laten we dit onderzoeken. Ten eerste zal de test mislukken. De $ _SESSION
variabele gebruikt in de Gebruiker
object bestaat niet in een eenheidscontrole, omdat het PHP uitvoert in de opdrachtregel.
Ten tweede is er geen verbinding met de database. Dit betekent dat, om dit te laten werken, we onze applicatie moeten booten om de App
object en zijn db
voorwerp. We hebben ook een werkende databaseverbinding nodig om te testen.
Om deze unit-test te laten werken, moeten we:
Laten we eens kijken hoe we dit kunnen verbeteren.
De functie die de huidige gebruiker ophaalt, is in deze eenvoudige context niet nodig. Dit is een gekunsteld voorbeeld, maar in de geest van DROGE principes, is de eerste optimalisatie die ik kies te maken deze methode te generaliseren.
class Gebruiker public function getUser ($ user_id) $ user = App :: db-> select ('user') -> where ('id', $ user_id) -> limit (1) -> get (); if ($ user-> num_results ()> 0) return $ user-> row (); return false;
Dit biedt een methode die we kunnen gebruiken in onze hele applicatie. We kunnen de huidige gebruiker op het moment van de oproep doorgeven, in plaats van die functionaliteit naar het model over te dragen. Code is modulair en onderhoudbaar als het niet afhankelijk is van andere functionaliteiten (zoals de globale variabele van de sessie).
Dit is echter nog steeds niet testbaar en onderhoudbaar als het zou kunnen zijn. We vertrouwen nog steeds op de databaseverbinding.
Laten we de situatie verbeteren door wat afhankelijkheidsinjectie toe te voegen. Dit is hoe ons model eruit zou kunnen zien als we de connectie van de database doorgeven aan de klas.
class User protected $ _db; publieke functie __construct ($ db_connection) $ this -> _ db = $ db_connection; openbare functie getUser ($ user_id) $ user = $ this -> _ db-> select ('user') -> where ('id', $ user_id) -> limit (1) -> get (); if ($ user-> num_results ()> 0) return $ user-> row (); return false;
Nu, de afhankelijkheden van onze Gebruiker
model zijn voorzien. Onze klasse gaat niet langer uit van een bepaalde databaseverbinding en vertrouwt ook niet op globale objecten.
Op dit punt is onze klasse in principe te testen. We kunnen een gegevensbron van onze keuze (meestal) en een gebruikers-id doorgeven en de resultaten van die oproep testen. We kunnen ook afzonderlijke databaseverbindingen uitschakelen (ervan uitgaande dat beide dezelfde methoden implementeren voor het ophalen van gegevens). Stoer.
Laten we eens kijken hoe een unit test eruit kan zien.
_mockDb (); $ user = new User ($ db_connection); $ result = $ user-> getUser (1); $ expected = new StdClass (); $ expected-> id = 1; $ expected-> gebruikersnaam = 'fideloper'; $ this-> assertEquals ($ result-> id, $ expected-> id, 'User-ID correct ingesteld'); $ this-> assertEquals ($ resultaat-> gebruikersnaam, $ verwachte-> gebruikersnaam, 'Gebruikersnaam correct ingesteld'); beschermde functie _mockDb () // "Mock" (stub) database rij resultaat object $ returnResult = new StdClass (); $ returnResult-> id = 1; $ returnResult-> gebruikersnaam = 'fideloper'; // Nep database-resultaatobject $ result = m :: mock ('DbResult'); $ result-> shouldReceive ('num_results') -> once () -> andReturn (1); $ result-> shouldReceive ('row') -> once () -> andReturn ($ returnResult); // Mock database-verbindingsobject $ db = m :: mock ('DbConnection'); $ db-> shouldReceive ('select') -> once () -> andReturn ($ db); $ db-> shouldReceive ('where') -> once () -> andReturn ($ db); $ db-> shouldReceive ('limit') -> once () -> andReturn ($ db); $ db-> shouldreceive ('get') -> once () -> andReturn ($ result); return $ db;
Ik heb iets nieuws toegevoegd aan deze unit test: Mockery. Met spot kun je "nep" (nep) PHP-objecten maken. In dit geval spotten we de databaseverbinding. Met onze mock kunnen we het testen van een databaseverbinding overslaan en eenvoudig ons model testen.
Wilt u meer weten over Mockery?
In dit geval bespotten we een SQL-verbinding. We vertellen het schijnobject dat het verwacht te hebben kiezen
, waar
, begrenzing
en krijgen
methoden die erop worden toegepast. Ik stuur de Mock zelf terug om te spiegelen hoe het SQL-verbindingsobject zichzelf teruggeeft ($ this
), waardoor de methode 'chainable' wordt genoemd. Merk op dat, voor de krijgen
methode, ik stuur het resultaat van de databaseaanroep terug - a stdClass
object met de gebruikersgegevens ingevuld.
Dit lost een paar problemen op:
We kunnen nog steeds veel beter doen. Hier wordt het interessant.
Om dit verder te verbeteren, kunnen we een interface definiëren en implementeren. Beschouw de volgende code.
interface UserRepositoryInterface openbare functie getUser ($ user_id); class MysqlUserRepository implementeert UserRepositoryInterface protected $ _db; publieke functie __construct ($ db_conn) $ this -> _ db = $ db_conn; openbare functie getUser ($ user_id) $ user = $ this -> _ db-> select ('user') -> where ('id', $ user_id) -> limit (1) -> get (); if ($ user-> num_results ()> 0) return $ user-> row (); return false; class User protected $ userStore; publieke functie __construct (UserRepositoryInterface $ user) $ this-> userStore = $ user; openbare functie getUser ($ user_id) return $ this-> userStore-> getUser ($ user_id);
Er gebeuren hier een paar dingen.
Voeg gebruiker toe()
methode.Gebruikersomgeving
in onze Gebruiker
model. Dit garandeert dat de gegevensbron altijd een getUser ()
methode beschikbaar, ongeacht welke gegevensbron wordt gebruikt om te implementeren Gebruikersomgeving
.Merk op dat onze
Gebruiker
objecttype-hintsGebruikersomgeving
in zijn constructor. Dit betekent dat een klasse wordt geïmplementeerdGebruikersomgeving
MOET worden doorgegeven aan deGebruiker
voorwerp. Dit is een garantie waarop we vertrouwen - we hebben degetUser
methode om altijd beschikbaar te zijn.
Wat is het resultaat hiervan?
Gebruiker
klasse, kunnen we gemakkelijk de gegevensbron bespotten. (Het testen van de implementaties van de gegevensbron zou de taak zijn van een afzonderlijke eenheidscontrole).Gebruiker
object als dat nodig is. Als u besluit SQL te dumpen, kunt u gewoon een andere implementatie maken (bijvoorbeeld, MongoDbUser
) en geef dat door aan jouw Gebruiker
model-.We hebben onze unit-test ook vereenvoudigd!
_mockUserRepo (); $ user = new User ($ userRepo); $ result = $ user-> getUser (1); $ expected = new StdClass (); $ expected-> id = 1; $ expected-> gebruikersnaam = 'fideloper'; $ this-> assertEquals ($ result-> id, $ expected-> id, 'User-ID correct ingesteld'); $ this-> assertEquals ($ resultaat-> gebruikersnaam, $ verwachte-> gebruikersnaam, 'Gebruikersnaam correct ingesteld'); beschermde functie _mockUserRepo () // Bespotten verwacht resultaat $ resultaat = nieuwe StdClass (); $ resultaat-> id = 1; $ result-> gebruikersnaam = 'fideloper'; // Bespotten van elke gebruikersrepository $ userRepo = m :: mock ('Fideloper \ Third \ Repository \ UserRepositoryInterface'); $ userRepo-> shouldReceive ('getUser') -> once () -> andReturn ($ result); return $ userRepo;
We hebben het werk van het bespotten van een databaseverbinding volledig overgenomen. In plaats daarvan bespotten we eenvoudigweg de gegevensbron en vertellen we wat we moeten doen wanneer getUser
wordt genoemd.
Maar we kunnen nog steeds beter doen!
Overweeg het gebruik van onze huidige code:
// In sommige controller $ user = new User (nieuwe MysqlUser (App: db-> getConnection ("mysql"))); $ user-> id = App :: session ("user-> id"); $ currentUser = $ user-> getUser ($ user_id);
Onze laatste stap zal zijn om te introduceren containers. In de bovenstaande code moeten we een aantal objecten maken en gebruiken om onze huidige gebruiker te krijgen. Deze code kan in uw toepassing liggen. Als je moet overschakelen van MySQL naar MongoDB, dan doe je dat nog steeds moet elke plaats bewerken waar de bovenstaande code verschijnt. Dat is nauwelijks DROOG. Containers kunnen dit verhelpen.
Een container "bevat" eenvoudig een object of functionaliteit. Het lijkt op een register in uw toepassing. We kunnen een container gebruiken om automatisch een nieuwe te instantiëren Gebruiker
object met alle benodigde afhankelijkheden. Hieronder gebruik ik Pimple, een populaire containerklasse.
// Ergens in een configuratiebestand $ container = nieuwe Pimple (); $ container ["user"] = function () nieuwe gebruiker teruggeven (nieuwe MysqlUser (App: db-> getConnection ('mysql'))); // Nu kunnen we in al onze controllers gewoon schrijven: $ currentUser = $ container ['user'] -> getUser (App :: session ('user_id'));
Ik heb de oprichting van de Gebruiker
model op één locatie in de toepassingsconfiguratie. Als gevolg:
Gebruiker
object en de data store naar keuze wordt op één locatie in onze applicatie gedefinieerd.Gebruiker
model van het gebruik van MySQL naar een andere gegevensbron in EEN plaats. Dit is veel beter houdbaar.In de loop van deze zelfstudie hebben we het volgende bereikt:
Ik weet zeker dat je hebt gemerkt dat we veel meer code hebben toegevoegd in de naam van onderhoudbaarheid en testbaarheid. Tegen deze implementatie kan een krachtig argument worden ingebracht: we worden steeds complexer. Inderdaad, dit vereist een diepere kennis van code, zowel voor de hoofdauteur als voor medewerkers van een project.
De kosten van uitleg en begrip zijn echter veel groter dan de extra kosten verminderen in technische schulden.
U mag opnemen Spot en PHPUnit in uw applicatie eenvoudig met behulp van Composer. Voeg deze toe aan uw "require-dev" -sectie in uw composer.json
het dossier:
"require-dev": "spot / spot": "0.8. *", "phpunit / phpunit": "3.7. *"
U kunt vervolgens uw Composer-afhankelijke afhankelijkheden installeren met de "dev" -vereisten:
$ php composer.phar install --dev
Lees hier meer over Mockery, Composer en PHPUnit op Nettuts+.
Overweeg om voor PHP Laravel 4 te gebruiken, omdat het uitzonderlijk gebruik maakt van containers en andere concepten die hier zijn beschreven.
Bedankt voor het lezen!