phpunit

Testes unitários com PHPUnit

logo_phpunit Hoje falarei sobre o principal framework para a realização de testes unitários no PHP, o PHPUnit.

Testes Unitários: O que são? Qual a importância?

Basicamente os testes unitários servem para verificar a integridade comportamental de unidades do sistema.

São testes feitos pelo programador, para validar o comportamento de cada unidade do sistema, na maioria das vezes métodos, dos quais são feitos testes nas mais diferentes condições sobre eles, validando o retorno de exceções, o conteúdo do retorno de um método etc. ajudando assim o programador a encontrar mais facilmente determinados erros ou comportamentos não esperados.

Se quiser entender mais os benefícios dos testes unitários, indico este artigo do André Thiago, do blog A Arte do Software: http://andrethiago.wordpress.com/2011/04/06/as-vantagens-do-teste-unitario/

Obtenção do PHPUnit

O PHPUnit pode ser obtido através do Composer pelo seguinte pacote: phpunit/phpunit.
Ou por download do arquivo PHAR (PHP Archive):

Nomenclatura dos testes

A nomenclatura deve ser o nome da classe a ser testada mais Test.

Exemplo:
Classe a ser testada: UsersEntity
Nome do teste: UsersEntityTest

Métodos

Os métodos dos testes não receberão argumentos e devem ser públicos. A nomenclatura de conter o prefixo test, além de serem auto-descritivos.

Exemplo:
Teste de um setter de nome: testSetterForName()

Métodos setUp() e tearDown()

Esses dois métodos são como o __construct() e o __descruct():

  • setUp(): será executado assim que o teste for iniciado. Geralmente utilizado para pré-configurações, como instanciação de conexão com banco de dados para testes etc.
  • tearDown(): será executado ao final do teste. Seguindo o exemplo anterior, desfazendo as alterações feitas no banco depois do teste, desfazendo a conexão etc.

Exemplo de teste

Primeiro vamos criar uma classe para ser testada:

class User{
    private $name;
    private $email;
    private $age;

    public function setName( $name ){
        if( !is_string( $name ) ){
            throw new InvalidArgumentException( 'Name must be a string' );
        }

        $this->name = $name;

        return $this;
    }

    public function setEmail( $email ){
        if( !is_string( $name ) ){
            throw new InvalidArgumentException( 'Email must be a string' );
        }

        $this->email = $email;

        return $this;
    }

    public function setAge( $age ){
        if( !is_int( $age ) ){
            throw new InvalidArgumentException( 'Age must be an integer' );
        }

        $this->age = $age;

        return $this;
    }

    public function getName(){
        return $this->name;
    }

    public function getEmail(){
        return $this->email;
    }

    public function getAge(){
        return $this->age;
    }
}

e o teste:

class UserTest extends PHPUnit_Framework_TestCase{
    public function assertPreConditions(){
        $this->assertTrue( class_exists( 'User' ), 'Class not found: User' );
    }

    /**
     * @expectedExcpetion InvalidArgumentExcpetion
     * @expectedExceptionMessage Name must be a string
    */
    public function testSetterForName(){
        $this->assertTrue( method_exists( new User, 'setName' ), 'Method not found: User::setName()' );

        $name = 'Gabriel Jacinto';
        $user = new User();
        $setName = $user->setName( $name );

        $this->assertEquals( $setName, $user, 'Method User::setName() must return a instance of User' );
        $this->assertAttributeEquals( $name, 'name', $user, 'Attribute was not correctly setted' );
    }
    /**
     * @depends testSetterForName
    */
    public function testGetterForName(){
        $this->assertTrue( method_exists( new User, 'getName' ) );

        $name = 'Gabriel Jacinto';
        $user = new User();
        $user->setName( $name );
        $getName = $user->getName();

        $this->assertEquals( $name, $getName, 'Method User::getName() must return the name setted in setName' );
    }
}

Explicando

  • public function assertPreConditions(){
        $this->assertTrue( class_exists( 'User' ), 'Class not found: User' );
    }
    

    O método assetPreConditions server para mostrar ao PHPUnit que é necessário este teste não falhar para os outros funcionarem. Ai está sendo necessário a classe User existir.Daqui a pouco explico o método assertTrue.

  • /**
     * @expectedExcpetion InvalidArgumentExcpetion
     * @expectedExceptionMessage Name must be a string
    */
    

    O PHPUnit lê e interpreta anotações feitas nas classes. Dentre elas, é possível indicar qual será a exception esperada caso haja algum erro na classe e qual a mensagem esperada por essa exception. Isso também é possível pelo método setExptectedException.

  • $this->assertTrue( method_exists( new User, 'setName' ), 'Method not found: User::setName()' );
    

    O método assertTrue verifica se uma afirmação é verdadeira, no caso a verificação se o método User::setName() estava presente na classe User. Como segundo argumento, é passada uma mensagem de erro caso o teste não passe.

  • $name = 'Gabriel Jacinto';
    $user = new User();
    $setName = $user->setName( $name );
    

    É a instanciação de User e a utilização do método setName, passando como o argumento a variável $name, que é uma string. Mas se pode fazer testes propositalmente errados para verificar se o teste realmente falha.

  • $this->assertEquals( $setName, $user, 'Method User::setName() must return a instance of User' );
    

    O método assertEquals verifica se dois valores são iguais, no caso verifica se o retorno do método setName é a própria instância da classe, no caso uma técnica chamada de Fluent Interface. Como terceiro argumento, é passada uma mensagem de erro caso o teste falhe.

  • $this->assertAttributeEquals( $name, 'name', $user, 'Attribute was not correctly setted' );
    

    O método assertAttributeEquals verifica se algum atributo de sua classe é igual a determinado valor. Ai testamos se a variável User::$name está com o valor de $name (Gabriel Jacinto), para verificar se foi alterada corretamente. O quarto argumento é para a mensagem de erro caso o teste falhe.

  • /**
     * @depends testSetterForName
    */
    

    Como já dito, o frameowrk interpreta anotações. @depends diz ao teste que o para ele funcionar, ele necessita que o teste indicado também funcione.

Bem, explicado tudo o que foi feito, podemos seguir com os outros testes:

/**
 * @exptectedException InvalidArgumentException
 * @exptectedExceptionMessage Email must be a string
*/
public function testSetterForEmail(){
    $this->assertTrue( method_exists( new User, 'setName' ) );

    $email = 'gamjj74@hotmail.com';
    $user = new User();
    $setEmail = $user->setEmail( $email );

    $this->assertEquals( $setEmail, $user, 'Method User::setEmail() must return a instance of User' );
    $this->assertAttributeEquals( $email, 'email', $user, 'Attribute was not correctly setted' );
}

/**
 * @depends testSetterForEmail
*/
public function testGetterForEmail(){
    $this->assertTrue( method_exists( new User, 'getName' ) );

    $email = 'gamjj74@hotmail.com';
    $user = new User();
    $user->setEmail( $email );
    $getEmail = $user->getEmail();

    $this->assertEquals( $email, $getEmail, 'getEmail must return the email setted in setEmail' );
}

/**
 * @exptectedException InvalidArgumentException
 * @exptectedExceptionMessage Age must be an integer
*/
public function testSetterForAge(){
    $this->assertTrue( method_exists( new User, 'setAge' ) );

    $age = 15;
    $user = new User();
    $setAge = $user->setAge( $age );

    $this->assertEquals( $setEmail, $user, 'Method User::setAge() must return a instance of User' );
    $this->assertAttributeEquals( $age, 'age', $user, 'Attribute was not correctly setted' );
}

/**
 * @depends testSetterForAge
*/
public function testGetterForAge(){
    $this->assertTrue( method_exists( new User, 'getAge' ) );

    $age = 15;
    $user = new User();
    $user->setAge( $age );
    $getAge = $user->getAge();

    $this->assertEquals( $age, $getAge, 'getAge must return the age setted in setAge' );
}

Este foi um pequeno exemplo com algumas demonstrações de testes, mas você pode encontrar tudo o que precisa na documentação do PHPUnit: todos os assertions, todas as anotações aceitas e muito mais.

Rodando os testes

Para rodar os testes, basta ir no terminal e indicar qual o diretório do teste e do arquivo do PHPUnit:

php vendor/phpunit/phpunit/composer/bin/phpunit Tests/UserTest.php

Bem, por hoje é só isso. Mostrei que testes unitários são importantes para o desenvolvimento de uma aplicação e um pouco do principal framework de testes unitários do PHP, que também é usado por grandes frameworks como o Zend.