Meu nome é Elton Minetto

Atoum: uma alternativa ao PHPUnit

php

Pesquisando novas ferramentas de testes e integração contínua, em especial o CircleCI, acabei encontrando um projeto interessante, o atoum. O projeto tem uma ambição grande: ser uma alternativa ao padrão do mercado, o PHPUnit.

O atoum é baseado nas novas features do PHP 5.3 e foi desenvolvido com as seguintes idéias:

  • Pode ser implementado rapidamente ;
  • Desenvolvimento simplificado de testes;
  • Permitir a criação de testes legíveis, confiáveis e claros;

Para poder comparar com o PHPUnit eu fiz um pequeno projeto de exemplo.

Instalação

Tanto o PHPUnit quanto o atoum possuem mais de uma opção de instalação, mas eu optei por usar o Composer, que deveria ser a forma oficial de instalação de qualquer projeto moderno. Para isso, primeiro instalei o Composer usando o comando:

curl -sS https://getcomposer.org/installer | php

No arquivo composer.json vamos incluir os dois frameworks:

{
    "require": {
        "atoum/atoum": "dev-master",
        "phpunit/php-timer": "1.0.4",
        "phpunit/phpunit-mock-objects": "1.2.*@dev",
        "phpunit/php-code-coverage": "1.2.*@dev",
        "phpunit/phpunit": "3.7.*@dev"
    }
}

E executar o comando:

php composer.phar install

Classe

O próximo passo foi criar uma classe para ser testada, no diretório src\Service\Auth.php:

<?php
namespace Service;

class Auth
{

	const INVALID_USER = 1; 
	const INVALID_PASSWORD = 2; 
	const VALID_AUTH = 3; 

	private $pdo;

	public function __construct(\PDO $pdo) 
	{
		$this->pdo = $pdo;
	}

	public function authenticate($login, $password)
	{
		//todo: filter parameters!
    	$sth = $this->pdo->prepare('select * from user where login = ?');
		$sth->bindParam(1, $login, \PDO::PARAM_STR);
		$sth->execute();
		$result = $sth->fetch(\PDO::FETCH_ASSOC);
		if (! $result) {
			return $this::INVALID_USER;
		}
		if ($password != $result['password']) {
			return $this::INVALID_PASSWORD;
		}
	
		return $this::VALID_AUTH;
	}
}

PHPUnit

Para facilitar a comparação, fiz primeiro o teste com a ferramenta que conheço, o PHPUnit.

Para isso criei o diretório tests\phpunit e criei dentro dela os seguintes arquivos e diretórios:

Service\AuthPHPUnitTest.php
bootstrap.php
phpunit.xml

O bootstrap.php e o phpunit.xml são arquivos auxiliares, sendo que no primeiro apenas configuro o loader do Composer e o segundo é o arquivo de configurações dos testes, com a inclusão do Code Coverage.

<?php

$loader = require __DIR__.'/../../vendor/autoload.php';
$loader->add('Service', __DIR__.'/../../src');
<phpunit
    bootstrap="bootstrap.php"
    colors="true"
    backupGlobals="false"
>
    <testsuites>
        <testsuite name="Test Suite">
            <directory>./Service</directory>
        </testsuite>
    </testsuites>

    <!-- Code Coverage Configuration -->
    <filter>
        <whitelist>
            <directory suffix=".php">../../src/</directory>
            <exclude>
                <directory suffix=".php">./</directory>
            </exclude>
        </whitelist>
    </filter>

    <logging>
        <log type="coverage-html" target="_reports/coverage" title="Coverage" charset="UTF-8" yui="true" highlight="true" lowUpperBound="35" highLowerBound="70"/>
        <log type="coverage-clover" target="_reports/logs/clover.xml"/>
        <log type="junit" target="_reports/logs/junit.xml" logIncompleteSkipped="false"/>
        <log type="testdox-text" target="_reports/testdox/executed.txt"/>
    </logging>
</phpunit>

No AuthPHPUnitTest.php estão os testes:

<?php

use Service\Auth;

class AuthPHPUnitTest extends \PHPUnit_Framework_TestCase
{

	private $validUser = 'kratos';
	private $validPassowrd = '1ca308df6cdb0a8bf40d59be2a17eac1';
    private $pdo;

	/**
     * Faz o setup dos testes
     * @return void
     */
    public function setup()
    {
        parent::setup();
        $this->pdo = new \PDO('sqlite:memory');
        $this->pdo->query('create table user (id INTEGER PRIMARY KEY AUTOINCREMENT, login text, password text)');
        $sth = $this->pdo->prepare('insert into user values(null,?,?)');
        $sth->bindParam(1, $this->validUser, \PDO::PARAM_STR);
        $sth->bindParam(2, $this->validPassowrd, \PDO::PARAM_STR);
        $sth->execute();
    }

    public function testInvalidUser() 
    {
    	$auth = new Auth($this->pdo);
    	$result = $auth->authenticate('invalidUser', $this->validPassowrd);

    	$this->assertEquals($result, Auth::INVALID_USER);
    }

    public function testInvalidPassword() 
    {
    	$auth = new Auth($this->pdo);
    	$result = $auth->authenticate($this->validUser, 'invalidPassword');

    	$this->assertEquals($result, Auth::INVALID_PASSWORD);
    }

    public function testValidAuth() 
    {
    	$auth = new Auth($this->pdo);
    	$result = $auth->authenticate($this->validUser, $this->validPassowrd);

    	$this->assertEquals($result, Auth::VALID_AUTH);
    }
}

Para executar os testes basta executar os comandos:

cd tests/phpunit/
../../vendor/bin/phpunit

E o resultado será apresentado no console. Se você tiver o XDebug instalado e configurado em seu PHP será criado um diretório chamado _reports com o relatório da cobertura de código da sua classe, o Code Coverage.

atoum

Vamos agora criar e configurar o atoum.

Criei um diretório para armazenar os testes, o tests\units e dentro dele criei a seguinte estrutura:

Service\Auth.php
bootstrap.php
coverage.php

O bootstrap.php e o coverage.php são, respectivamente o arquivo de bootstrap dos testes e a configuração da cobertura de códigos:

<?php
//bootstrap.php
$loader = require __DIR__.'/../../vendor/autoload.php';
$loader->add('Service', __DIR__.'/../../src');
<?php
//coverage.php
use \mageekguy\atoum;

$coverageHtmlField = new atoum\report\fields\runner\coverage\html('Your project name', '_reports');
$coverageHtmlField->setRootUrl('http://url/of/web/site');
$coverageTreemapField = new atoum\report\fields\runner\coverage\treemap('Your project name', '_reports');
$coverageTreemapField
	->setTreemapUrl('http://url/of/treemap')
	->setHtmlReportBaseUrl($coverageHtmlField->getRootUrl())
;
$script
	->addDefaultReport()
		->addField($coverageHtmlField)
		->addField($coverageTreemapField)
;

E o Auth.php contém os testes:

<?php
namespace tests\units\Service;

include __DIR__ . '/../bootstrap.php';

use mageekguy\atoum\test;
use Service\Auth as AuthService;
use mageekguy\atoum\reports;

class Auth extends test
{
	private $validUser = 'kratos';
	private $validPassowrd = '1ca308df6cdb0a8bf40d59be2a17eac1';
    private $pdo;

    public function beforeTestMethod($testMethod) {
        $this->pdo = new \PDO('sqlite:memory');
        $this->pdo->query('create table user (id INTEGER PRIMARY KEY AUTOINCREMENT, login text, password text)');
        $sth = $this->pdo->prepare('insert into user values(null,?,?)');
        $sth->bindParam(1, $this->validUser, \PDO::PARAM_STR);
        $sth->bindParam(2, $this->validPassowrd, \PDO::PARAM_STR);
        $sth->execute();
    }

    public function testInvalidUser()
    {
 		$auth = new AuthService($this->pdo);
    	$result = $auth->authenticate('invalidUser', $this->validPassowrd);
    	$this->assert->integer($result)
    				 ->isEqualTo(AuthService::INVALID_USER);
    }

    public function testInvalidPassword() 
    {
    	$auth = new AuthService($this->pdo);
    	$result = $auth->authenticate($this->validUser, 'invalidPassword');

    	$this->assert->integer($result)
    				 ->isEqualTo(AuthService::INVALID_PASSWORD);
    }

    public function testValidAuth() 
    {
    	$auth = new AuthService($this->pdo);
    	$result = $auth->authenticate($this->validUser, $this->validPassowrd);

    	$this->assert->integer($result)
    				 ->isEqualTo(AuthService::VALID_AUTH);
    }
}

Para executar os testes:

cd tests/units
../../vendor/atoum/atoum/bin/atoum -c coverage.php Service/Auth.php

No diretório _reports vai ser gerado o relatório em HTML da cobertura de códigos do seu projeto, desde que você tenha o XDebug instalado.

Conclusões

A primeira diferença que percebi foi na hora de criar os testes. É obrigatório que o teste seja criado no namespace tests\units e que o nome da classe de testes seja igual ao nome da classe que você está testando. No teste usando PHPUnit o nome da classe que escrevi foi AuthPHPUnitTest e para o atoum foi preciso criar a classe com o nome Auth ou o framework não rodava.

Eu gostei do formato “fluido” dos testes do atoum permitindo que você encadeie os assertions, ficando o código dos testes mais legíveis. Ele possui uma boa biblioteca de assertions nativos para serem usados nos testes.

O atoum também possui um componente para fazermos mocks mas não cheguei a testar nesse exemplo para poder chegar a uma conclusão sobre ele em comparação ao do PHPUnit ou o Mockery.

Quanto a performance, o atoum me pareceu mais rápido do que o concorrente, mas como são poucos testes neste exemplo não consegui chegar a uma conclusão efetiva sobre isso.

O atoum pode ser usado em conjunto com o Jenkins, mas eu achei o relatório de Code Coverage do PHPUnit bem mais amigável. Existem documentações e projetos no Github que mostram como integrá-lo também ao Symfony e ao Zend Framework 2.

Como o projeto foi desenvolvido por um francês boa parte da documentação ainda não foi traduzida para o inglês, muito menos para o português, mas no site oficial já existem bons exemplos e textos.

Ainda é cedo para dizer se o PHPUnit corre algum risco, mas gostei muito do que vi e vou acompanhar a evolução do projeto de perto