PHPUnit och testa modul som är beroende av Anax MVC

By . Latest revision .

Hur gör man om man vill använda PHPUnit för att testa en modul som har ett beroende till ett ramverk som Anax MVC? Kan man testa modulen men ändå inkludera hela ramverket? Finns det något enkelt sätt att göra det på?

Här får du ett exempel på hur du kan enhetstesta en modul som använder sig av delar av Anax MVC. I artikeln visas hur modulen kan implementera tester för ett externt beroende till Anax MVC och dessutom visas hur du hanterar dependency injection med $di så att modulen får del av ramverkets alla tjänster.

Denna artikel bygger vidare på artiklarna “Använd PHPUnit och Travis för att enhetstesta modul som har externt beroende” och “PHPUnit och testa modul som är beroende av databas”. Artiklarna använder samma källkod som exempelkod som baskod.

#Innan vi sätter igång

Jag utgår från version v5.0 av mosbth/mumin. Jag utvecklar exemplet i en branch v6. När jag är klar med detta exempel så taggar jag den färdiga koden som v6.0.

Du kan checka ut din egen kopia så här och växla mellan versionerna.

$ git clone https://github.com/mosbth/mumin.git
$ git checkout v5.0
$ git checkout v6
$ git checkout v6.0

Du kan växla mellan olika brancher och taggar via git checkout.

#Modul beroende av Anax MVC och $di

Jag tänker mig en modul som är beroende av sessionen och sessionen hanteras av $di->session i Anax MVC. Jag gör min modul så att den läser och skriver till sessionen. Min modul behöver alltså tillgång till koden för Anax MVC.

Jag skapar jag en ny klass MumintrolletSession som ärver från Mos\Mumin\Mumintrollet.

class MumintrolletSession extends \Mos\Mumin\Mumintrollet implements \Anax\DI\IInjectionAware
{
    use \Anax\DI\TInjectable;

Min klass implementerar även interfacet \Anax\DI\IInjectionAware genom att använda traitet \Anax\DI\TInjectable. Det betyder att klassen är redo för att ta emot och jobba med ramverkets tjänster via $di.

Jag implementerar två metoder som använder sig av $di->session.

/**
 * Get name from session.
 *
 * @return string
 *
 */
public function getNameFromSession()
{
    if ($this->session->has('name')) {
        return $this->session->get('name');
    } else {
        return "No name is set in session.";
    }
}



/**
 * Set name in session.
 *
 * @return void
 *
 */
public function setNameInSession($name)
{
    $this->session->set('name', $name);
}

Den ena metoden läser från sessionen och den andra skriver till sessionen.

Metoderna förutsätter att klassen har tillgång till $di enligt den implementationen som traitet \Anax\DI\TInjectable erbjuder.

Så här ser den färdiga koden ut i sitt sammanhang.

#Testfall för metoderna

Jag skapar en testklass och kodar testfallen för klassen. Jag tänker börja med en liten testmetod och sedan testa mig fram steg för steg tills det fungerar.

/**
 * Test
 *
 * @return void
 *
 */
public function testGetNameFromSession()
{
    $mumin = new \Mos\Mumin\MumintrolletSession();
    $di    = new \Anax\DI\CDIFactoryDefault();
    $mumin->setDI($di);

    $name = "Snusmumriken";
    $mumin->setNameInSession($name);

    $name2 = $mumin->getNameFromSession();
    $this->assertEquals($name, $name2, "The name does not match.");
}

Jag skapar en instans av klassen och injektar $di. Sedan skriver jag koden för att testa klassens metoder.

Låt se, vilka problem får jag nu… jag testar att köra phpunit.

$ phpunit                                   
PHPUnit 4.1.0 by Sebastian Bergmann.                           
                                                               
Configuration read from /home/mos/git/mumin/phpunit.xml.dist   
                                                               
...PHP Fatal error:  Interface 'Anax\DI\IInjectionAware' not found in
/home/mos/git/mumin/src/Mumin/MumintrolletSession.php on line 10

Hmmm. Okey, det får vi lösa.

#Gör require på Anax MVC

Min modul är beroende av klasser i Anax MVC och behöver ha tillgång till dess kod. Detta löser jag på samma sätt som jag gjorde i artikeln “PHPUnit och testa modul som är beroende av databas”. Jag gör helt enkelt require på Anax MVC med composer.

I min composer.json lägger jag till så att ramverket inkluderas.

"require": {
    "anax/mvc": "dev-master"
}

Sedan går jag till mitt repo på kommandoraden och uppdaterar med composer.

$ composer update --no-dev
Loading composer repositories with package information
Updating dependencies
  - Installing anax/mvc (dev-master 4194347)
    Cloning 4194347232e130b8d8bbecc59c92365b521a8aab

Writing lock file
Generating autoload files

Så där. Nu ligger hela Anax MVC i katalogen vendor/anax och autoloadern sköter så att alla filer hittas.

Då testar jag med phpunit igen.

$ phpunit                                   
PHPUnit 4.1.0 by Sebastian Bergmann.                           
                                                               
Configuration read from /home/mos/git/mumin/phpunit.xml.dist

...E.

Time: 242 ms, Memory: 13.25Mb

There was 1 error:

1) Mos\Mumin\MumintrolletSessionTest::testGetNameFromSession
Use of undefined constant ANAX_APP_PATH - assumed 'ANAX_APP_PATH'

/home/mos/git/mumin/vendor/anax/mvc/src/DI/CDIFactoryDefault.php:20
/home/mos/git/mumin/test/Mumin/MumintrolletSessionTest.php:21

Okey, ett steg framåt. Då tar vi nästa problem.

#Anax behöver sin omgivning

Anax MVC behöver två sökvägar för att fungera. Det är ANAX_INSTALL_PATH och ANAX_APP_PATH.

Jag lägger till följande rader i min test/config.php.

/**
 * Define essential Anax paths, end with /
 *
 */
define('ANAX_INSTALL_PATH', realpath(__DIR__ . '/../vendor/anax/mvc') . '/');
define('ANAX_APP_PATH',     ANAX_INSTALL_PATH . 'app/');

Då testar jag igen.

PHPUnit 4.1.0 by Sebastian Bergmann.

Configuration read from /home/mos/git/mumin/phpunit.xml.dist

...E.

Time: 248 ms, Memory: 13.25Mb

There was 1 error:

1) Mos\Mumin\MumintrolletSessionTest::testGetNameFromSession
Exception: In trait TInjectable used by class Mos\Mumin\MumintrolletSession.
    You are trying to get a property (service) "session" from $this->di, but 
    the service is not set in $this->di. Did you misspell the service you 
    are trying to reach or did you forget to load it into the $di
    container?CDI could not load service 'session'. Failed in the callback 
    that instantiates the service. session_start(): Cannot send session 
    cookie - headers already sent by (output started
 at phar:///usr/local/bin/phpunit/phpunit/Util/Printer.php:172)

/home/mos/git/mumin/vendor/anax/mvc/src/DI/TInjectable.php:58
/home/mos/git/mumin/src/Mumin/MumintrolletSession.php:40
/home/mos/git/mumin/test/Mumin/MumintrolletSessionTest.php:25

Ytterligare ett steg framåt.

Men uppenbarligen kan jag inte starta tjänsten $di->session.

#Sätt upp sessionen för tester

Nej, man kan inte skapa sessionen när man kör PHP via kommandoraden som vi gör med PHPUnit. Men vi kan fortfarande testa modulen och Anax MVC som om sessionen fanns där. Vi låter bara bli att starta sessionen.

Men för att göra det behöver jag modifiera själva uppstarten av tjänsten – så som den startas i \Anax\DI\CDIFactoryDefault. Jag löser det genom att omdefiniera hur tjänsten $di->session skapas. Jag gör det direkt i mitt testfall så att du kan se koden i ett sammanhang.

Så här ser mitt uppdaterade testfall ut.

/**
 * Test
 *
 * @return void
 *
 */
public function testGetNameFromSession()
{
    $mumin = new \Mos\Mumin\MumintrolletSession();
    $di    = new \Anax\DI\CDIFactoryDefault();
    $mumin->setDI($di);

    $di->setShared('session', function () {
        $session = new \Anax\Session\CSession();
        $session->configure(ANAX_APP_PATH . 'config/session.php');
        $session->name();
        //$session->start();
        return $session;
    });

    $name = "Snusmumriken";
    $mumin->setNameInSession($name);

    $name2 = $mumin->getNameFromSession();
    $this->assertEquals($name, $name2, "The name does not match.");
}

Det är alltså precis som tidigare, bortsett från att jag nu kommenterad bort $session->start(). Tjänsten session fanns sedan tidigare i $di, men det går alltså bra att omdefiniera den.

Då kör vi igen.

$ phpunit
PHPUnit 4.1.0 by Sebastian Bergmann.

Configuration read from /home/mos/git/mumin/phpunit.xml.dist

.....

Time: 255 ms, Memory: 13.25Mb

OK (5 tests, 5 assertions)

Generating code coverage report in Clover XML format ... done

Generating code coverage report in HTML format ... done

Vips. Så bra det kan gå ibland. Det fungerar.

Här är den färdiga koden för testklassen.

#Commit, push och låt Travis jobba

Så, jag tar och committar mitt arbete, fortfarande är jag i min branch v6. Jag pushar och låter Travis jobba. Förhoppningsvis förstår nu även Travis hur den skall använda composer för att göra require på Anax MVC och därefter köra enhetstesterna med phpunit.

Det ser ut som det gick bra.

Allt fungerar och jag mergar min branch med min master.

$ git checkout master
$ git merge v6
$ git commit -a -m "phpunit with Anax MVC dependency"
$ git push

Det gick bra. Ibland blir man förvånad när – synbarligen avancerade saker – bara fungerar. Men det har väl med att göra att man blir bättre och bättre på det man gör.

Jag taggar denna versionen av Mumin som v6.0.

Här är länken till den sista builden på Travis, så kan du se att det gick bra.

#Avslutningsvis

Som du ser så kan ett ramverk vara en modul i sig. Ibland får man fundera på vem som inkluderar vem. Men det blir faktiskt rätt smidigt i längden - det här med moduler, composer och packagist. Bara man får ordning på grunderna.

Tekniken som jag använder när jag testar sessionen är att fylla den med de värden som jag kan använda för mina tester. Om jag nu skulle testa en klass som hanterar inloggning så skulle jag göra på samma sätt. Jag behöver bara sätta rätt saker i sessionen inför varje testfall.

Detta är ett bra sätt att testa saker i sessionen. Men taktiken fungerar lika bra när man testar kod som har ett beroende mot $_GET, $_POST och $_SERVER – som du normalt implementerar via ramverkets tjänster $di->request.

Det finns ett foruminlägg för denna artikel om du vill diskutera eller ställa frågor.

#Revision history

  • 2015-01-12: (A, mos) Första utgåvan.

Document source.

Category: php.