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.