Testaaminen WordPress-kehityksessä

Testaaminen on ohjelmistokehityksen perusjuttuja ja tapa varmistaa, että koodi jota tehdään, on toimivaa. Testaamista on monenlaista, tässä keskityn yksikkötestaamiseen (unit testing) ja integraatiotestaamiseen (integration testing). Näissä erona on, että yksikkötestauksessa testataan ohjelmiston yksittäisiä komponentteja ja integraatiotestauksessa isompia kokonaisuuksia. WordPress-kontekstissa näitä kahta on kuitenkin hankalaa ja tarpeetontakin erottaa tarkasti toisistaan. Kolmas olennainen testaamisen muoto on regressiotestaus (regression testing), jossa varmistellaan, että aikaisemmin toimivat ominaisuudet eivät ole menneet rikki.

Kirjoitus on laadittu Mac OS -käyttäjän näkökulmasta, sillä oletuksella että Homebrew on käytössä ja perustyökalut kuten wget ja Composer on asennettu. Linux-käyttäjät osannevat käyttää omaa paketinhallintaansa tarvittavien työkalujen asentamiseen, Windows-käyttäjät joudun valitettavasti jättämään oman onnensa nojaan.

Testaustyökalut

Tarkastelen testaamista ensisijaisesti lisäosakehittäjän näkökulmasta, koska se on oma näkökulmani. Periaatteet lienevät sovellettavissa muuallekin, etenkin sellaisiin sivustoprojekteihin, joihin sisältyy paljon omaa koodia, usein omana lisäosana toteutettuna.

PHPUnitin logo

PHPUnit. WordPressiä testattaessa perustyökalu on PHPUnit. Tätä kirjoittaessani uusinta versiota 8 ei vielä voi käyttää WordPressin testaamiseen, versio 7 sen sijaan toimii mainiosti. Aloita siis asentamalla PHPUnit.

Wp-test-framework. Jotta testejä voidaan ajaa, tarvitaan testiympäristö. Otto Rask on kehittänyt erittäin näppärän testipaketin, joka huolehtii kaikesta. Otetaan siis käyttöön wp-test-framework, joka tulee näppärästi käyttöön Composer-riippuvuutena:

composer require --dev rask/wp-test-framework

Kun riippuvuus on määritelty composer.json-tiedostoon, framework asennetaan yksinkertaisesti ajamalla

composer update

Sen jälkeen framework on käytettävissä.

WordPress. Lisäksi tarvitaan WordPress. Sitä ei tarvitse edes asentaa, riittää että tiedostot ovat saatavilla jossain. Jos käytössä on WordPress CLI, asennus käy näin kätevästi:

wp core download

Ilman WP CLI:tä homma hoituu lähes yhtä näppärästi näin:

wget https://wordpress.org/latest.zip
unzip latest.zip

WordPressin hakemistoon on vain lisättävä testiasetukset sisältävä wp-tests-config.php-tiedosto, josta löytyy mallikappale WordPressin repositoriosta. Tähän tiedostoon täytyy vain päivittää tietokannan asetukset.

WP_TESTS_INSTALLATION-ympäristömuuttuja säädetään osoittamaan tähän asennushakemistoon, esimerkiksi näin:

export WP_TESTS_INSTALLATION=/Users/msaari/wordpress/

MySQL-tietokanta. Mistä päästäänkin palapelin viimeiseen palaseen, eli käytössä on oltava tietokanta. Jos koneelta löytyy jo valmiiksi tietokantapalvelin (esimerkiksi MAMP:n muodossa), käytä sitä, muussa tapauksessa MariaDB on vaivaton asennettava:

brew install mariadb
mysql.server start

Tarkista, että wp-tests-config.php-tiedostossa on oikeat tiedot tietokannalle. Huomaa, että testejä varten täytyy olla täysin oma tietokantansa, koska testien aluksi tietokannasta tuhotaan aina kaikki taulut. Samassa tietokannassa ei siis voi pitää mitään muuta.

PHPUnitin asetukset

Nyt kun testijärjestelmän osat ovat koossa, on aika rakennella varsinaisia testejä. Projektin juurihakemistoon luodaan tiedosto phpunit.xml, jossa määritellään, miten testit ajetaan. Relevanssin asetukset näyttävät tältä:

<phpunit
	bootstrap="tests/bootstrap.php"
	backupGlobals="false"
	colors="true"
	convertErrorsToExceptions="true"
	convertNoticesToExceptions="true"
	convertWarningsToExceptions="true"
	>
	<php>
    	<ini name="display_errors" value="true"/>
    </php>
	<testsuites>
		<testsuite name="Relevanssi">
			<directory prefix="test-" suffix=".php">./tests/</directory>
			<directory prefix="test-" suffix=".php">./premium-tests/</directory>
		</testsuite>
	</testsuites>
	<filter>
        <whitelist>
            <directory suffix=".php">./lib</directory>
            <directory suffix=".php">./premium</directory>
        </whitelist>
    </filter>
</phpunit>

Osa on ihan perusasetuksia, osa taas sitten projektikohtaista. Tärkeä on bootstrap-asetus, jossa määritellään tiedosto, josta löytyy testien bootstrap-ohjeet eli se, miten järjestelmä käynnistetään. Siitä kohta lisää.

Tiedostossa määritellään testsuite, jossa on määritelty, että testit löytyvät hakemistoista tests ja premium-tests tiedostoista, joiden nimet alkavat ”test-”. Lopun whitelist-filtteri liittyy testikattavuusasioihin, eli se kertoo, mistä hakemistoista löytyy koodi, josta testikattavuutta mitataan.

Bootstrap-tiedosto

Tiedoston tests/bootstrap.php sisältö taas on yksinkertaisuudessaan tämä:

require_once './vendor/autoload.php';
\rask\WpTestFramework\Framework::load();

require_once dirname( __DIR__ ) . '/relevanssi.php';

Ensimmäiset kaksi riviä lataavat ja käynnistävät Raskin wp-test-frameworkin ja viimeinen rivi sisällyttää mukaan Relevanssin.

Mitään tavanomaista lisäosanasennusprosessia tässä ei ole käytössä, vaan tarvittavat lisäosat asennetaan näin, sisällyttämällä niiden tiedostot requirella.

Testien kirjoittaminen

Testitiedostot ovat rakenteeltaan luokkia, jotka laajentavat WP_UnitTestCase-luokkaa. Alkuvalmistelut tehdään julkisessa staattisessa funktiossa wpSetupBeforeClass(), lopuksi ajetaan wpTearDownAfterClass() ja siinä välissä suoritetaan kaikki julkiset funktiot, joiden nimi alkaa test.

Relevanssin tapauksessa jokaisessa luokassa ajetaan ensin lisäosan asennusfunktio relevanssi_install() ja normaalisti init-koukussa ajettava relevanssi_init() – koska sitä init-koukkua ei testien yhteydessä suoriteta – ja sitten lopuksi siivotaan jäljet ajamalla relevanssi_uninstall(). Näin eri testikokonaisuudet eivät häiritse toisiaan.

Luokat on jaoteltu sen mukaan, mitä toiminnallisuutta ne testaavat. Noin karkeasti jokainen luokka liittyy erilliseen tiedostoon. Omat luokkansa on muun muassa indeksoinnin, hakemisen, otteiden ja korostusten, käyttöliittymän, hukkasanojen ja taksonomiakyselyjen testaamiselle.

Yksittäisessä testifunktiossa testataan jotain tiettyä asiaa suorittamalla Relevanssin funktioita ja tekemällä oletuksia palautusarvoista. Tavallisin testioletus on $this->assertEquals(), joka valittaa virheestä, jos vertailuarvo ei vastaa oletusta. Yksikkötestissä voidaan siis suorittaa jostain pienestä yksityiskohdasta vastaava funktio, jonka palautusarvo eri syötteillä tiedetään ja sitten vain vertaillaan, vastaako funktiosta tuleva palautusarvo sitä, mitä sen pitäisi olla.

PHPUnit-testien virheettä läpimennyt suoritus.
Jos testit menevät läpi, niin tältä se sitten näyttää.

Vinkkejä testien koodaamiseen

Jos kokonaisuus koostuu riittävän pienistä funktioista, yksikkötestien kirjoittaminen on melko suoraviivaista. Testaaminen kannustaakin pilkkomaan liian isoja funktioita pienempiin osiin, joita on sitten helpompi testata. Tällöin erilaisia alkuvalmisteluitakin tarvitaan vähemmän: yleensä riittää, että funktiolle annetaan oikeanlainen syöte.

WordPressin lisäosia testatessa tulee kuitenkin jonkin verran tarvetta laajemmille integraatiotesteille. Esimerkiksi Relevanssin kohdalla voidaan toki testata yksittäisiä funktioita, mutta on myös hyvä testata, että koko hakuprosessi menee läpi niinkuin pitää, tai että tietokannan indeksointi kokonaisuudessaan toimii. Se vaatii vähän alkuvalmisteluja ja sopivan sisällön rakentelemista tietokantaan.

Testifunktioissa on mahdollista luoda artikkeleita ja muuta sisältöä WordPressin funktioilla. Artikkelien luominen wp_insert_post()-funktiolla toimii aivan hyvin. WordPressin testipaketissa on myös jokunen apufunktio ja tehdas, joilla voi luoda sisältöä. Esimerkiksi monta artikkelia voi luoda kerralla funktiolla $this->factory->post->create_many( 10 ), joka antaa paluuarvona listan luotujen artikkeleiden ID-tunnuksista. Näille ei ole olemassa dokumentaatiota, eli tarkempi perehtyminen asiaan edellyttää lähdekoodin tutkailua.

Testifunktioilla voi olla riippuvuuksia toisistaan. Jos funktiolle määrittelee riippuvuuden merkitsemällä kommenttiblokkiin @depends test_toinen_testi(), tämä jälkimmäinen funktio suoritetaan ensimmäisen jälkeen ja se saa parametrinään edeltävän funktion return-arvon. Kannattaa huomata, että funktiot voivat vaikuttaa toisiinsa muutenkin. Kaikki oletukset esimerkiksi asetuksien suhteen kannattaa ottaa huomioon ja kaikki jotain testiä varten säädetyt asetukset nollata testin jälkeen, muuten saa ihmetellä mikä on, kun joku testi menee läpi kun sen suorittaa yksinään, mutta ei osana isompaa kokonaisuutta.

Tässä on esimerkki funktiosta, joka testaa kuvaliitteiden indeksoinnin estävää asetusta.

/**
 * Tests the relevanssi_no_image_attachments function and feature.
 */
public function test_no_image_attachments() {
	$this->delete_all_posts();

  	$image_attachment = array(
		'post_title'     => 'cat gif',
		'post_mime_type' => 'image/gif',
		'post_type'      => 'attachment',
		'post_status'    => 'publish',
	);
	wp_insert_post( $image_attachment );

	$pdf_attachment = array(
		'post_title'     => 'cat pdf',
		'post_mime_type' => 'application/pdf',
		'post_type'      => 'attachment',
		'post_status'    => 'publish',
	);
	wp_insert_post( $pdf_attachment );

	update_option( 'relevanssi_index_post_types', array( 'attachment' ) );

	update_option( 'relevanssi_index_image_files', 'off' );
	$return = relevanssi_build_index( false, null, null, true );
	$this->assertEquals(
		1,
		$return['indexed'],
		"With image files excluded, the number of posts indexed isn't correct."
	);

	update_option( 'relevanssi_index_image_files', 'on' );
	$return = relevanssi_build_index( false, null, null, true );
	$this->assertEquals(
		2,
		$return['indexed'],
		"With image files included, the number of posts indexed isn't correct."
	);
}

Testissä poistetaan kaikki artikkelit $this->delete_all_posts()-apufunktiolla, luodaan sitten kaksi artikkelia, kuvaliite ja PDF-muotoinen liite (varsinaisia liitetiedostoja ei ole, koska niillä ei ole merkitystä tämän testin kannalta), ja sen jälkeen säädetään Relevanssi indeksoimaan liitteet. Kuvaliitteiden indeksointi kytketään pois päältä ja tietokanta indeksoidaan relevanssi_build_index()-funktiolla.

Jos kaikki menee oikein, $return['indexed'] eli indeksoitujen artikkelien lukumäärä on yksi. Sitten kuvaliitteiden indeksointi sallitaan ja uusitaan indeksointi, jolloin $return['indexed'] pitäisi saada arvokseen kaksi.

Mitä pitäisi testata?

Millaisia juttuja sitten kannattaa testata? Tätä ihmettelin aluksi kovasti. Päädyin rakentelemaan testejä tavallisimmille toiminnallisuuksille ja vastaan tulleille bugeille. Sitten opin, että on olemassa asia nimeltä testikattavuus, jolla voidaan tarkistaa, mitkä kaikki ohjelmakoodin osat on testattu, ja sen jälkeen olen ottanut asiakseni käydä kaiken koodin läpi niin, että testikattavuus olisi sataprosenttinen.

Tavoite ei ole ihan täysin mielekäs, mutta se on ollut kuitenkin hyödyllinen. Tämän tavoitteen saavuttamiseksi olen käynyt Relevanssin koodia läpi tiheällä kammalla, refaktoroinut sitä uuteen, järkevämpään malliin ja siinä sivussa löytänyt runsaasti bugeja ja puutteita esimerkiksi taksonomiakyselyistä – ne ovat hankala aihe, jossa on helppo tehdä virheitä, ja huolellinen ja perinpohjainen testaaminen on auttanut kaivamaan esiin monta bugia ja epäloogisuutta, joita kymmenen vuotta vanhaan koodimassaan on varmasti pesiytynyt joka puolelle.

Testikattavuusraportit saa tehtyä PHPUnitilla. Tarjolla on muutama erilainen malli, minä olen käyttänyt tätä:

phpunit --coverage-html coverage

Tämä tekee coverage-hakemistoon HTML-muotoisen raportin, josta näkee miltä osin koodi on testattu ja mitä osia koodista ei suoriteta lainkaan. Raportteihin sisältyy Change Risk Anti-Patterns eli CRAP-indeksi (tuttavallisemmin paskaindeksi), joka perustuu syklomaattiseen kompleksisuuteen ja testikattavuuteen ja kertoo yhden näkemyksen siitä, miten vaikeaselkoista koodi on. Tästä saa olla monta mieltä, mutta oma näkemykseni on, että pienempi CRAP-lukema tarkoittaa selkeämpää ja helppotajuisempaa koodia ja on siksi hyvä asia. Kun testikattavuus lähestyy sataa prosenttia ja CRAP-lukema pienenee, koodista tulee miellyttävämpää.

Relevanssin testikattavuusraportti.
Testikattavuus on paikoin hyvällä mallilla, paikoin on vielä täysin nollaa – mutta kaikkea tästä ei ole mahdollista tai järkevää testata sataprosenttisesti.

Se mitä Relevanssin testipaketti ei lainkaan testaa tällä hetkellä on yhteensopivuudet muiden lisäosien kanssa. Tähänkin ehkä jossain kohtaa päästään, mutta siinä, että saa testiasennukseen sisällytettyä tarvittavat muut lisäosat ja viriteltyä ne koodin tasolla testaustilanteen vaatimaan kuntoon, on sen verran paljon työtä, että en ole siihen vielä lähtenyt.

Tarkemmin Relevanssin testikokonaisuutta voi tutkailla GitHub-reposta, josta löytyy ilmaisversion testit. Relevanssi Premiumin asennuspaketin mukana tulee täysi sarja testejä myös Premiumin ominaisuuksille. WordPress.orgista saatavassa ilmaisversion asennuspaketissa ei ole mitään testejä mukana.

Testaamisprosessi

Kuvasin aikaisemmin omaa kehitysprosessiani. Siihen testit asettuvat siten, että ensinnäkin kehitystyön aikana ajan testejä kehitysversiota vasten – kun jotain on tehty, tsekataan phpunitilla että mitään ei mennyt rikki ja tarvittaessa lisätään uusia testejä uutta toiminnallisuutta varten. Varsinkin ihan uusia ominaisuuksia luotaessa testien kirjoittamisen pitäisi olla osa kehitystyötä.

Vielä enemmän testaustyötä tehdään silloin, kun uuden kehittämisen sijasta refaktoroidaan vanhaa ja parannetaan testikattavuutta.

Myös julkaisemiseen liittyy oma testausprosessinsa. Jotta julkaistava versio voidaan päästää käyttäjille asti, se on testattava asennuspakettina. Tämä varmistaa, että asennuspaketti on toimiva (aina ei ole ollut). Premiumissa tämä tapahtuu julkaisemalla uusi versio versionumerolla 0. Ilmaisversiota testataan tagaamalla uusi versio GitHubiin, jolloin se voidaan asentaa Composerilla.

Minulla on testihakemisto, jossa on tällainen composer.json:

{
  "repositories": [
  { 
    "type": "package", 
    "package": {  
      "name": "relevanssi/relevanssi-premium",
      "version": "0", 
      "type": "wordpress-plugin",
      "dist": { 
        "type": "zip", 
        "url": "https://www.relevanssi.com/update/get_version.php?api_key=123456&version=0"
      }
    }
  },
  {
    "type": "vcs",
    "url": "https://github.com/msaari/relevanssi"
  }
  ],
  "require": {
    "msaari/relevanssi":"4.3.4",
    "relevanssi/relevanssi-premium": "*"
  }
}

Tällä asentuu sekä ilmaisversion tuorein julkaisu GitHubista että Premiumin testiversio. Sen jälkeen voin ajaa tämän skriptin:

#!/bin/sh

rm -rf vendor
composer clear-cache
composer update

cd vendor/relevanssi/relevanssi-premium/
composer update

sh multi-version-test.sh

cd ../../msaari/relevanssi
composer update

sh multi-version-test.sh

Ensin tyhjennetään vanhat versiot poistamalla vendor-hakemisto, tyhjennetään Composerin välimuisti ja päivitetään Relevanssit.

Sitten siirrytään Relevanssi Premiumin hakemistoon, ajetaan sielläkin composer update, joka asentaa wp-test-frameworkin paikalleen ja sitten suoritetaan testiskripti, joka ajaa Relevanssin testit läpi useammalla WordPressin versiolla. Sen jälkeen sama toistetaan ilmaisversiolle.

Näin molemmat versiot on testattu ja mikäli kaikki testit menevät läpi kunnialla, uudet versiot voidaan julkaista, kuten prosessijutussa kuvattiin.

Testaaminen monella WordPressin versiolla

Tätä vaihetta voisi vielä avata. Aikaisemmin testasin vain yhdellä WordPressin versiolla – uusimmalla, jos olin muistanut päivittää testiasennuksen – mutta sitten keksin, että aika pienellä lisävaivalla voisi testata useammankin version: jos eri WordPress-versiot ovat omissa hakemistoissaan, riittää, että WP_TESTS_INSTALLATION-ympäristömuuttujan arvoa vaihdetaan testien välillä.

No, innostuin sitten vähän kirjoittamaan skriptausta tehtävän automatisoimiseksi ja päädyin multi-version-test.sh-skriptiin. Se on liian pitkä tähän liitettäväksi, joten vilkuilkaa tuolta GitHubista samalla kun selitän.

Skripti edellyttää, että on määritelty hakemisto, johon WordPressit asennetaan ja josta löytyy valmiiksi säädetty wp-tests-config.php. Sen jälkeen skriptissä määritellään, mikä on vanhin WordPressin versio, jolla halutaan testata.

Skripti käy hakemassa WordPressin API:sta tiedon saatavilla olevista versioista (tällä PHP-skriptillä), siis kunkin versiosarjan tuoreimman version. Tällä hetkellä lista menisi 5.3, 5.2.4, 5.1.3, 5.0.7, 4.9.12 ja niin edelleen.

Jokaisen testattavan version kohdalla tarkistetaan, onko se asennettu jo, eli löytyykö esimerkiksi hakemistoa wordpress-5.2.4 jo testihakemistosta. Jos ei löydy, skripti hakee tiedoston WordPressin palvelimelta wgetillä, purkaa paketin, siirtää sen oikeaan hakemistoon ja laittaa wp-tests-config.php -tiedoston paikoilleen.

Jos versio löytyi tai saatiin asennettua, sille ajetaan testit. Jos multisite-testien määritystiedosto löytyy (eli kyseessä on Premium) ja versio on vähintään 5.1, testit ajetaan multisitenä, muuten ajetaan yhden sivuston testit. Testit ajetaan --stop-on-failure-parametrin kanssa, eli yksikin virhe riittää pysäyttämään testin, tämä säästää aikaa ja virheen sattuessa voi sitten halutessaan tutkia tarkemmin, mistä on kyse.

Useamman version testaamista kerralla.
Testit menevät näköjään läpi ainakin versioilla 5.3 ja 5.2.4.

Vanhin versio, jota tällä järjestelyllä voi testata on 5.0, koska wp-test-framework sisältää asioita, jotka ovat tulleet mukaan versiossa 5.0.0. 4.9-versioita ehkä vielä pitäisi testata, sitä monet Gutenbergin pelossa vielä käyttävät (mutta kannattaisi jo pikkuhiljaa siirtyä 5-versioihin – jos et Gutenbergistä pidä, Classic Editor on tarjolla), mutta olettaisin, että 5.0 vastaa 4.9:ää riittävän hyvin.

Testauksen automatisointia

Vielä parempaa tietysti olisi, jos testausta saisi automatisoitua siten, että versionhallintaan pusketut versiot testattaisiin automaattisesti. Tällaisia CI/CD-putkiahan tarjotaan moneltakin taholta, käyttämässäni GitLabissa sellainen olisi myös tarjolla, mutta toistaiseksi olen todennut, että tässä tapauksessa testausympäristö on vaikkapa suoraviivaista Node-sovellusta sen verran monimutkaisempi rakennella, että se ylittää minun osaamiseni rajat.

Jos jollakulla sattumoisin on kokemusta WordPress-yhteensopivan testausympäristön kehittelystä vaikkapa GitLabin CI-putkeen, minuun saa olla yhteydessä. Nyt tyydyn siis toteamaan, että toimii se näinkin, ja käytän aikani mieluummin varsinaiseen kehitystyöhön kuin testausympäristön rakentelemiseen.

Vastaa

Sähköpostiosoitettasi ei julkaista.

This site uses Akismet to reduce spam. Learn how your comment data is processed.