Teszteljünk konténerben

CC0 image by StockSnap

Tavaly, mikor nekiálltam a Docker-es sorozatnak, terveztem még egy negyedik részt is, ami a tesztelésről szólt volna. Aztán annak rendje és módja szerint meg is feledkeztem róla. Egészen mostanáig.

A példák PHP-t használnak a tesztelt alkalmazás nyelveként, de a koncepciók hasonlóan működhetnek bármilyen más nyelven is. A teljes kód pedig szokás szerint megtalálható a példa repóban. Vágjunk is bele.

Unit tesztek

Az ide vonatkozó Docker Compose konfiguráció annyira egyszerű, hogy igazából csak a teljesség kedvéért került bele ebbe a bejegyzésbe is. A nagy részét megtárgyaltuk már a sorozat első részének "Hello Composer" fejezetében.

docker-compose.yml
version: "3"
services:
  app:
    image: composer:1.3
    volumes:
      - .:/app
    working_dir: /app

A teszteket pedig a következő paranccsal tudjuk futtatni:

$ docker-compose run --rm -T app vendor/bin/phpunit

A könnyed felvezető után ugorjunk is inkább az izgalmasabb részekre.

Integrációs tesztek

A témakör elég tág, így két dolgot is meg fogunk vizsgálni közelebbről. Az adatbázist használó és a külső szolgáltatással kommunikáló kódok tesztjeit.

Adatbázis

Itt, hasonlóan a korábbi "Hello MySQL" részhez, szükségünk lesz egy adatbázis szervere:

docker-compose.yml
# ...
  database:
    image: mysql:5.7
    volumes:
      - ./etc/mysql/init.sql:/docker-entrypoint-initdb.d/init.sql:ro
    environment:
      MYSQL_ROOT_PASSWORD: test
      MYSQL_USER: test
      MYSQL_PASSWORD: test
# ...

Az init.sql segítségével létrehozunk két adatbázist, az egyiket a fejlesztéshez szeretnénk majd használni, a másikat a tesztek futtatásához. Ezen kívül az init.sql még beállítja a jogosultságokat és létre is hozza mindkettőben a megfelelő sémát.

Ami ezen a ponton feltűnhet, hogy az adatbázis tartalmának nem csináltunk saját volume-ot. Ennek az oka pedig az, hogy most nem csak egy konfigurációs fájlt fogunk használni. Lesz egy külön konfigurációnk a fejlesztéshez:

docker-compose.dev.yml
# ...
volumes:
  mysql:
# ...
  database:
    volumes:
      - mysql:/var/lib/mysql
# ...

Itt Docker volume-ot használunk az adatok tárolására, hogy azok a konténerek leállítása után is megmaradjanak. A Docker Compose-t pedig a következőképpen tudjuk rábírni, hogy mindkét fájlt használja:

$ docker-compose -f docker-compose.yml -f docker-compose.dev.yml up -d

Mivel az az elgondolás, hogy fejlesztés közben az alkalmazást is aktívan használjuk, ezért a teszteket futtathatjuk exec segítségével a már futó konténerben:

$ docker-compose -f docker-compose.yml -f docker-compose.dev.yml exec -T app vendor/bin/phpunit

A másik konfigurációnk a build-hez lesz:

docker-compose.build.yml
# ...
  database:
    tmpfs:
      - /var/lib/mysql
# ...

Itt tmpfs-t használunk az adatok tárolására, mivel nem is igazán akarjuk őket tárolni, ellenben szeretnénk ha gyors lenne, a tmpfs pedig memóriában tárolódik.

A build esetén az elvárásaink is kicsit mások, ami a tesztek futtatását illeti. Az alkalmazás még nem fut és szeretnénk azt is megvárni, hogy az adatbázis elinduljon, mielőtt elkezdenénk futtatni a teszteket.

$ docker-compose -f docker-compose.yml -f docker-compose.build.yml up -d database
$ docker-compose -f docker-compose.yml -f docker-compose.build.yml exec -T database bash -c 'while ! mysqladmin ping -hdatabase -u$$MYSQL_USER -p$$MYSQL_PASSWORD --silent; do sleep 1; done'
$ docker-compose -f docker-compose.yml -f docker-compose.build.yml run -T --rm app vendor/bin/phpunit
$ docker-compose -f docker-compose.yml -f docker-compose.build.yml down
  1. Elindítjuk az adatbázist
  2. A mysqladmin ping parancs segítségével megvárjuk, hogy ténylegesen el is induljon
  3. Lefuttatjuk a tesztjeinket
  4. Lekapcsolunk mindent.

Mint az a példából is gyönyörűen látszik, kezdenek egyre hosszabbak lenni ezek a parancsok, senki se gépelné be ezeket egynél többször, ha nem muszáj. Ennek kiküszöbölésére továbbra is tudom ajánlani a Makefile használatát, amire szintén lehet példát találni a kapcsolódó repóban.

API

Egy másik dolog, ami problémákat okozhat integrációs teszteknél, ha valamilyen API-val kommunikálunk. Ennek megoldására felhúzhatunk egy egyszerű service-t, ami mondjuk a PHP beépített webszerverét használja és egy olyan könyvtárszerkezetet szolgál ki, ami leutánozza a tesztekben használt API-t.

docker-compose.yml
# ...
  api:
    image: php:7.1-alpine
    command: php -S 0.0.0.0:80 -t /app/
    volumes:
      - ./etc/api/:/app/:ro
# ...

Az etc/api/ könyvtár pedig valahogy így nézhet ki:

  • v2/
    • users/
      • 1234/
        • index.php
      • index.php

Az index.php megvalósítása lehet nagyon egyszerű, már-már statikus, de ízlés szerint elbonyolíthatjuk akár input validációval, authentikációval vagy egyéb dolgokkal is.

Funkcionális tesztek

Ez is egy többféleképpen megközelíthető kategória. Előfordulhat, hogy az általunk használt keretrendszer nyújt segítséget, és csak emulálunk http kéréseket (mint például a Silex féle WebTestCase). Az is lehet egy megoldás, hogy valós http kéréseket küldünk az alkalmazásnak és az érkező válaszokat vizsgáljuk. Mindkét eset a Docker Compose konfigurációja szempontjából leginkább a unit tesztek megoldására hajaz.

No de mi van olyankor, ha egy igazi böngészőben szeretnénk automatizáltan kattintgatni? Ebben a Selenium és a Codeception lehet a segítségünkre. Szerencsénkre a Selenium volt olyan kedves, hogy szolgáltasson Docker image-eket is:

docker-compose.yml
# ...
  browser:
    image: selenium/standalone-chrome
    volumes:
      - /dev/shm:/dev/shm
# ...

Már csak a Codeception-nek kell megmondani, hogy ezt használja:

tests/acceptance.suite.yml
# ...
    - WebDriver:
      url: http://web
      host: browser
      browser: chrome
# ...

A web itt az alkalmazásunkat futtató service neve, a browser pedig a fent definiált Selenium service. Eredetileg az alkalmazást itt is app-nak hívtam, mint a többi példában, de valamilyen rejtélyes oknál fogva úgy nem működött.

Jól látszik, hogy ahogy nő az egy teszttel megfuttatott kód mennyisége, úgy lesz bonyolultabb a hozzá felhúzott teszt infrastruktúra is. A példában nem tértünk rá ki külön, de a funkcionális teszteknél jó eséllyel szükségünk lesz majd az integrációs teszteknél használt módszerekre is, hogy egy megfelelő állapotban lévő alkalmazást tudjunk a böngészőben tesztelni.