Unit testen, Magento en de database

Share

Het schrijven van goede, test-driven code is belangrijk voor de stabiliteit en garantie van een project, maar het helpt je ook – als ontwikkelaar – om ‘s avonds beter te kunnen slapen. In dit artikel laten we je zien hoe we unit tests hebben ingezet voor een Magento project waarbij berekeningen met de database cruciaal waren.

Het scenario

Een grote klant van ons had een zeer specifieke wens voor een kortingsmodule voor hun nieuwe webshop. De korting welke een klant kreeg werd namelijk berekend aan de hand van de organisatie waar de klant aan toegewezen was. Dus de prijs voor product X kon verschillen tussen klant A en B.

Alsof dat nog niet genoeg was: de product catalogus werd ook nog gevuld door externe toeleveranciers, waarvan sommige leveranciers dezelfde producten leverden. Als dat het geval was, moest de leverancier met de goedkoopste prijs de order krijgen. Maar klanten konden ook verschillende kortingen krijgen voor verschillende toeleveranciers. Daardoor kan het dus voorkomen dat als bijvoorbeeld leverancier A de goedkoopste is, dat door de korting die jij specifiek als klant krijgt leverancier B alsnog goedkoper is, waardoor deze dus de order krijgt!

Ben je een beetje in de waar geraakt? Geeft niet hoor. Dit is gewoon een beetje achtergrondinformatie, maar het toont aan dat de wens van de klant al snel complex wordt met veel parameters waar rekening mee gehouden moet worden. Hierdoor is een fout snel gemaakt.

Unit testing en databases

Meteen vanaf het begin was het wel duidelijk dat we unit tests moesten gaan schrijven voor deze module om de garantie en werking van de module te kunnen waarborgen. Maar hoe test je iets zoals dat? Alle prijzen van producten, leveranciers, organisaties en kortingsregels welke alles samenvoegen zijn opgeslagen in de database, en ook alle berekeningen zijn – voor optimalisatie redenen – grotendeels gedaan in enorme SQL-queries.

Bij het schrijven van heen unit test vind ik het altijd belangrijk om de test zo simpel en op zichzelf mogelijk te houden. Heel kort door de bocht zou je moeten kunnen zeggen: “als ik deze functie uitvoer met deze parameters dan verwacht ik dit resultaat”. Maar als je databases meeneemt in de berekening worden zaken vrij snel vrij complex. Een heleboel gegevens en relaties welke invloed hebben op je berekeningen zijn opgeslagen in de database – een losse entiteit – en als hier mutaties in plaatsvinden (records die wijzigen of relaties die verwijderd worden) is de kans groot dat je tests het niet meer zullen doen. Het is tevens belangrijk dat als je in een team werkt je collega’s ook de tests uit kunnen voeren. En de kans is groot dat hun lokale database op hun ontwikkelomgeving afwijkt van jouw eigen database. Dus hoe ga je hiermee om?

Maak test tabellen in de database

Het eerste wat je moet doen om dit probleem te tackelen is toegewijde tabellen in je database maken met testdata. In mijn situatie had ik gewoon nieuwe tabellen gemaakt aan de hand van de schema’s van de bestaande tabellen en ze voorzien van het voorvoegsel “test_”. Vervolgens had ik ze gevuld met voorbeeld data. Met deze data is het mogelijk om het te verwachte resultaat (handmatig) te berekenen. Uiteindelijk kun je al deze testtabellen in een los SQL-bestand zetten en toevoegen aan je repository zodat je collega’s ook deze testtabellen aan kunnen maken in hun lokale ontwikkelomgeving (of zelfs nog beter zoals we verderop zullen bespreken: je laat de unit test deze tabellen + data voor je aanmaken).

Maak je testsuite

Nu dat we de testdata in onze database hebben kun je je testsuite gaan maken. In mijn situatie had ik een map gemaakt genaamd   Tests  in mijn module map, met daarin het bestand   phpunit.xml

Dit is een vrij basis setup zoals je kunt zien. Zoals ik eerder al aangaf geef ik er de voorkeur aan om zaken zo simpel mogelijk te houden. Wat ik heb gedaan is het volgende::

  • Ik open het bestand genaamd   bootstrap.php . Dit bestand wordt uitgevoerd voordat de volledige testsuite uitgevoerd wordt.
  • Ik definieer de constante  MAGENTO_ROOT , welke intern door Magento gebruikt wordt. (normaal gesproken wordt deze constante gedefinieerd in   index.php , maar die ingang gebruiken we niet in onze test dus we moeten hem handmatig instellen).
  • Het beginpunt van mijn volledige testsuite is de huidige directory.

De bootstrap

Het  bootstrap.php  bestand wordt uitgevoerd voordat de volledige testsuite begint en doet de volgende zaken:

  • Initialiseren van Magento.
  • De test tabellen opnieuw instellen / importeren voor iedere keer dat de testsuite gedraait wordt. (voor het geval dat we bijvoorbeeld CRUD acties uitvoeren).

Het bestand ziet er als volgt uit:

De test

Het mooie van deze bootstrap is dat deze iedere keer dat de test draait (opnieuw) de test tabellen in de database maakt. Hierdoor weten we precies met welke data we aan het testen zijn en ook wat voor resultaten we zouden moeten verwachten. Een bijkomend voordeel van het bundelen van de SQL-data met de test is dat je collega’s (of testserver) met dezelfde testdata kunnen testen. Hiermee elimineer je dus het probleem met verschillende databases onder verschillende installaties.

Maar eerst  een beetje achtergrondinformatie: zoals ik van tevoren al had gezegd wordt de kortingsberekening gedaan in enorme SQL-queries in Magento. Als je unit-testbare code wilt schrijven in Magento moet je hier dus rekening mee houden. Dat houdt dus in dat je niet direct de tabelnamen in je SQL-queries moet plaatsen, maar gebruik moet maken van Magento’s standaard functionaliteit om tabelnamen uit de configuratie te lezen. Sterker nog: je zult altijd zo je SQL-queries op moeten zetten, omdat het de Magento-conventie is:

1
2
$resource = Mage::getSingleton(‘core/resource’);
$sql      = ‘SELECT * FROM ‘ . $resource->getTableName(‘discount/rule’) . ‘;’;

Dit moet namelijk zo omdat de   getTableName() -functie in het bovenstaande voorbeeld de sleutelwoorden   discount  en  rule  om zal zetten naar het volgende XPath voor je configuratie:   global/models/discount_resource/entities/rule/table . Hiermee wordt de tabelnaam uitgelezen uit de configuratie. Maar als je al enige ervaring hebt met het schrijven van modules, dan wist je dit waarschijnlijk al. Een mooi bijkomend voordeel hiervan is dat je Magento configuratie ad-hoc kunt vervangen. Zo kun je er bijvoorbeeld voor zorgen dat de   getTableName() -functie een andere tabelnaam retourneert als hij gebruikt wordt in een unit test. Op deze manier kan dus precies dezelfde code gebruikt worden op 2 verschillende sets van data.

Laten we bijvoorbeeld de regel   global/models/discount_resource/entities/rule/table  nemen: deze zal standaard als tabelnaam “discount_rules” retourneren. Maar ik heb ook een test-tabel genaamd “test_discount_rules”. Als ik deze wil gebruiken kan ik dit simpelweg doen door de volgende regel toe te voegen in mijn script:

Het toevoegen van deze regel heeft alleen invloed op de huidige uitvoer en de configuratie wordt niet letterlijk opgeslagen. Dit maakt het perfect voor een unit test. Neem bijvoorbeeld een test als volgt:

Door de bovenstaande code toe te voegen aan het   setUp() -onderdeel van je unit test zorgt ervoor dat Magento je testtabellen gebruikt iedere dat de   getTableName() -methode wordt gebruikt (wat normaliter alleen in SQL queries is). En dat is eigenlijk de enige voorbereiding welke je moet doen voor je test. Je kunt nu gewoon je methodes testen en aanroepen zoals je ze in je eigen scripts zou gebruiken, en kijken of de geretourneerde resultaten zijn wat je verwacht.

Bekijk bijvoorbeeld maar eens naar deze 2 tests welke controleren of de korting voor een bepaald product/klant-combinatie correct berekend wordt:

En zo heb je ineens perfect unit testbare code in Magento, waarbij de unit test queries uitvoert op de database – net als op de productie site – maar waarbij we de testdata- en resultaten toe kunnen voegen aan ons versiebeheer zodat het ook door collega’s en testservers gebruikt kan worden. Amazing!

Conclusie

In dit artikel heb ik je een uitdaging laten zien waar ik tegen aan liep en hoe ik deze heb opgelost. Ik heb tests geschreven welke controleren of complexe SQL-queries onder verschillende omstandigheden de juiste resultaten terug stuurden, waarbij ik de tests zo simpel en op zich zelfstaand mogelijk heb gehouden.

Unit testen is een geweldige tool welke programmeurs in staat stelt hun code structureel te testen. Het biedt garantie en stabiliteit op de lange termijn en het helpt met het snel vinden en oplossen van problemen. Het is dan ook niet voor niets dat Happy Online op zo veel mogelijk belangrijke en kritieke plekken unit tests in te zetten in onze oplossingen.

Het is natuurlijk op vele manieren mogelijk om een unit test in te richten en dit is één van de mogelijkheden. Plus daarbij blijft het veld van webdevelopment (en dus ook het testen daarvan) een vak dat continu in ontwikkeling is. Het kan daardoor zijn dat je zelf misschien een betere aanpak kent hoe om te gaan met dit soort vraagstukken. Als je vragen of commentaar hebt op dit artikel, aarzel dan niet om een reactie achter te laten. Wij zijn altijd bereidwillig om zaken extra uit te leggen, maar blijven ook leergierig om andere inzichten te bekijken.

Comments are closed.